@ogygia/devtools 0.0.0-stage → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +25 -2
- 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 +103 -3
|
@@ -0,0 +1,2638 @@
|
|
|
1
|
+
import { another_routes_file } from "./route-files.js";
|
|
2
|
+
import { sequential_ms } from "./net.js";
|
|
3
|
+
import { browser_findings, browser_page_report } from "./browser-findings.js";
|
|
4
|
+
import { memo_candidates, span_values, sync_io } from "./insights.js";
|
|
5
|
+
import { fmt_bytes, page_score } from "./score.js";
|
|
6
|
+
import { unread_rows } from "./drill.js";
|
|
7
|
+
import { render_steps } from "./steps.js";
|
|
8
|
+
import { own_requests } from "./own-requests.js";
|
|
9
|
+
import { io_kind } from "./async-io.js";
|
|
10
|
+
import { PHASE_LABEL, chain_steps } from "./timeline.js";
|
|
11
|
+
import { DOM_LARGE, TESTING_ENGINE_VERSION, browser_of, engine_of, explain_held_open, fp_compare, hazard_fix, hazard_words, inline_threshold_tune, late_found, lcp_font, lcp_rivals, runtime_scripts, vital_parts } from "@ogygia/core/insights";
|
|
12
|
+
import { unscoped_finding } from "@ogygia/core/internal/instruments";
|
|
13
|
+
// src/profiler/report.ts
|
|
14
|
+
/** a file's name from its URL, for a score detail line */
|
|
15
|
+
function asset_name(url) {
|
|
16
|
+
const q = url.indexOf("?");
|
|
17
|
+
const path = q === -1 ? url : url.slice(0, q);
|
|
18
|
+
return path.slice(path.lastIndexOf("/") + 1) || path;
|
|
19
|
+
}
|
|
20
|
+
/** The union of intervals' length: what overlapping spans cost in wall time. */
|
|
21
|
+
function wall_of(ivs) {
|
|
22
|
+
const sorted = [...ivs].sort((a, b) => a[0] - b[0]);
|
|
23
|
+
let wall = 0;
|
|
24
|
+
let cur = null;
|
|
25
|
+
for (const [a, b] of sorted) if (cur && a <= cur[1]) cur[1] = Math.max(cur[1], b);
|
|
26
|
+
else {
|
|
27
|
+
if (cur) wall += cur[1] - cur[0];
|
|
28
|
+
cur = [a, b];
|
|
29
|
+
}
|
|
30
|
+
if (cur) wall += cur[1] - cur[0];
|
|
31
|
+
return wall;
|
|
32
|
+
}
|
|
33
|
+
/** FAIR SHARES of overlapping intervals: at every instant the wall is split evenly among the
|
|
34
|
+
* intervals open then, so the shares add up to exactly the union's length. For 242 renders run
|
|
35
|
+
* together, one's start-to-end (97 ms) is mostly waiting on the other 241; its share (0.8 ms) is
|
|
36
|
+
* its part of the 185 ms they cost. Also the peak number open at once. O(n log n): the share of
|
|
37
|
+
* [a, b] is S(b) − S(a), where S is the running integral of 1 / (number open). */
|
|
38
|
+
function fair_shares(ivs) {
|
|
39
|
+
const ev = [];
|
|
40
|
+
for (const [a, b] of ivs) {
|
|
41
|
+
ev.push([a, 1]);
|
|
42
|
+
ev.push([b, -1]);
|
|
43
|
+
}
|
|
44
|
+
ev.sort((x, y) => x[0] - y[0] || x[1] - y[1]);
|
|
45
|
+
const times = [];
|
|
46
|
+
const integral = [];
|
|
47
|
+
let open = 0;
|
|
48
|
+
let peak = 0;
|
|
49
|
+
let s = 0;
|
|
50
|
+
let t_prev = ev.length ? ev[0][0] : 0;
|
|
51
|
+
for (const [t, d] of ev) {
|
|
52
|
+
if (open > 0) s += (t - t_prev) / open;
|
|
53
|
+
t_prev = t;
|
|
54
|
+
open += d;
|
|
55
|
+
if (open > peak) peak = open;
|
|
56
|
+
times.push(t);
|
|
57
|
+
integral.push(s);
|
|
58
|
+
}
|
|
59
|
+
const at = (t) => {
|
|
60
|
+
let lo = 0;
|
|
61
|
+
let hi = times.length - 1;
|
|
62
|
+
let k = 0;
|
|
63
|
+
while (lo <= hi) {
|
|
64
|
+
const mid = lo + hi >> 1;
|
|
65
|
+
if (times[mid] <= t) {
|
|
66
|
+
k = mid;
|
|
67
|
+
lo = mid + 1;
|
|
68
|
+
} else hi = mid - 1;
|
|
69
|
+
}
|
|
70
|
+
return integral[k] ?? 0;
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
shares: ivs.map(([a, b]) => Math.max(0, at(b) - at(a))),
|
|
74
|
+
peak
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Fold the recording's spans per name. */
|
|
78
|
+
function span_rows(spans) {
|
|
79
|
+
if (!spans?.length) return [];
|
|
80
|
+
const by = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const s of spans) {
|
|
82
|
+
let g = by.get(s.name);
|
|
83
|
+
if (!g) by.set(s.name, g = {
|
|
84
|
+
list: [],
|
|
85
|
+
callers: /* @__PURE__ */ new Map()
|
|
86
|
+
});
|
|
87
|
+
g.list.push(s);
|
|
88
|
+
if (s.caller) g.callers.set(s.caller, (g.callers.get(s.caller) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
91
|
+
const children = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const s of spans) if (s.parent !== void 0) (children.get(s.parent) ?? children.set(s.parent, []).get(s.parent)).push(s);
|
|
93
|
+
const self_of = (s) => {
|
|
94
|
+
if (s.open || s.ms < 0) return 0;
|
|
95
|
+
const kids = children.get(s.id);
|
|
96
|
+
if (!kids?.length) return s.ms;
|
|
97
|
+
const end = s.start + s.ms;
|
|
98
|
+
const inside = kids.filter((k) => !k.open && k.ms >= 0).map((k) => [Math.max(k.start, s.start), Math.min(k.start + k.ms, end)]).filter(([a, b]) => b > a);
|
|
99
|
+
return Math.max(0, s.ms - wall_of(inside));
|
|
100
|
+
};
|
|
101
|
+
return [...by.entries()].map(([name, g]) => {
|
|
102
|
+
const done = g.list.filter((s) => !s.open && s.ms >= 0).map((s) => s.ms).sort((a, b) => a - b);
|
|
103
|
+
const hit = g.list.filter((s) => s.attrs?.cache === "hit").length;
|
|
104
|
+
const miss = g.list.filter((s) => s.attrs?.cache === "miss");
|
|
105
|
+
const keys = /* @__PURE__ */ new Set();
|
|
106
|
+
for (const s of g.list) for (const k of Object.keys(s.attrs ?? {})) if (k !== "cache") keys.add(k);
|
|
107
|
+
const finished = g.list.filter((s) => !s.open && s.ms >= 0);
|
|
108
|
+
const wall = wall_of(finished.map((s) => [s.start, s.start + s.ms]));
|
|
109
|
+
const fair = fair_shares(finished.map((s) => [s.start, s.start + s.ms]));
|
|
110
|
+
const share = new Map(finished.map((s, i) => [s, fair.shares[i]]));
|
|
111
|
+
const share_sorted = [...fair.shares].sort((a, b) => a - b);
|
|
112
|
+
const by = {};
|
|
113
|
+
for (const k of keys) {
|
|
114
|
+
const groups = /* @__PURE__ */ new Map();
|
|
115
|
+
let usable = true;
|
|
116
|
+
for (const s of finished) {
|
|
117
|
+
const v = s.attrs?.[k];
|
|
118
|
+
if (v === void 0) continue;
|
|
119
|
+
if (typeof v !== "string" && typeof v !== "boolean") {
|
|
120
|
+
usable = false;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
const key = String(v);
|
|
124
|
+
(groups.get(key) ?? groups.set(key, []).get(key)).push(s);
|
|
125
|
+
if (groups.size > 60) {
|
|
126
|
+
usable = false;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!usable || groups.size < 2 || groups.size > g.list.length / 2) continue;
|
|
131
|
+
const rows = [...groups].map(([value, list]) => {
|
|
132
|
+
const sorted = list.map((s) => s.ms).sort((a, b) => a - b);
|
|
133
|
+
return {
|
|
134
|
+
value,
|
|
135
|
+
count: list.length,
|
|
136
|
+
total_ms: r2(sorted.reduce((a, c) => a + c, 0)),
|
|
137
|
+
wall_ms: r2(wall_of(list.map((s) => [s.start, s.start + s.ms]))),
|
|
138
|
+
share_ms: r2(list.reduce((a, s) => a + (share.get(s) ?? 0), 0)),
|
|
139
|
+
p50_ms: sorted[Math.floor(sorted.length / 2)] ?? 0,
|
|
140
|
+
max_ms: sorted.at(-1) ?? 0
|
|
141
|
+
};
|
|
142
|
+
}).sort((a, b) => b.wall_ms - a.wall_ms || b.total_ms - a.total_ms);
|
|
143
|
+
if (rows.length > 12) {
|
|
144
|
+
const rest = rows.splice(12);
|
|
145
|
+
rows.push({
|
|
146
|
+
value: `(${rest.length} more)`,
|
|
147
|
+
count: rest.reduce((a, r) => a + r.count, 0),
|
|
148
|
+
total_ms: r2(rest.reduce((a, r) => a + r.total_ms, 0)),
|
|
149
|
+
wall_ms: r2(rest.reduce((a, r) => a + r.wall_ms, 0)),
|
|
150
|
+
share_ms: r2(rest.reduce((a, r) => a + r.share_ms, 0)),
|
|
151
|
+
p50_ms: 0,
|
|
152
|
+
max_ms: Math.max(...rest.map((r) => r.max_ms))
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
by[k] = rows;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
name,
|
|
159
|
+
count: g.list.length,
|
|
160
|
+
total_ms: r2(done.reduce((a, c) => a + c, 0)),
|
|
161
|
+
wall_ms: r2(wall),
|
|
162
|
+
self_ms: r2(finished.reduce((a, s) => a + self_of(s), 0)),
|
|
163
|
+
by,
|
|
164
|
+
share_p50_ms: r2(share_sorted[Math.floor(share_sorted.length / 2)] ?? 0),
|
|
165
|
+
peak: fair.peak,
|
|
166
|
+
p50_ms: done[Math.floor(done.length / 2)] ?? 0,
|
|
167
|
+
max_ms: done.at(-1) ?? 0,
|
|
168
|
+
errors: g.list.filter((s) => s.error).length,
|
|
169
|
+
open: g.list.filter((s) => s.open).length,
|
|
170
|
+
...hit || miss.length ? { cache: {
|
|
171
|
+
hit,
|
|
172
|
+
miss: miss.length,
|
|
173
|
+
miss_ms: r2(miss.reduce((a, s) => a + Math.max(s.ms, 0), 0))
|
|
174
|
+
} } : {},
|
|
175
|
+
callers: [...g.callers.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([c]) => c),
|
|
176
|
+
attr_keys: [...keys].sort()
|
|
177
|
+
};
|
|
178
|
+
}).sort((a, b) => b.total_ms - a.total_ms);
|
|
179
|
+
}
|
|
180
|
+
const fmt_ms = (n) => n >= 100 ? n.toFixed(0) : n >= 10 ? n.toFixed(1) : n.toFixed(2);
|
|
181
|
+
const fmt_pct = (part, whole) => whole > 0 ? (part / whole * 100).toFixed(1) + "%" : "—";
|
|
182
|
+
const round1 = (n) => Math.round(n * 10) / 10;
|
|
183
|
+
/** The dev server's own packages: their work (module loading, transforms) exists only in dev. */
|
|
184
|
+
const DEV_TOOLS = /* @__PURE__ */ new Set([
|
|
185
|
+
"vite",
|
|
186
|
+
"rolldown",
|
|
187
|
+
"rollup",
|
|
188
|
+
"esbuild",
|
|
189
|
+
"vite-node",
|
|
190
|
+
"vitefu",
|
|
191
|
+
"postcss",
|
|
192
|
+
"lightningcss",
|
|
193
|
+
"@sveltejs/vite-plugin-svelte"
|
|
194
|
+
]);
|
|
195
|
+
/** A sampled origin that is a dev tool's: `fn (vite)` — a package in the brackets, not a `file:line`. */
|
|
196
|
+
function dev_tool_caller(caller) {
|
|
197
|
+
const open = caller.lastIndexOf("(");
|
|
198
|
+
if (open === -1 || !caller.endsWith(")")) return false;
|
|
199
|
+
const pkg = caller.slice(open + 1, -1);
|
|
200
|
+
return DEV_TOOLS.has(pkg) || pkg.startsWith("@rolldown/") || pkg.startsWith("@vitejs/");
|
|
201
|
+
}
|
|
202
|
+
const CALL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
203
|
+
"if",
|
|
204
|
+
"for",
|
|
205
|
+
"while",
|
|
206
|
+
"switch",
|
|
207
|
+
"return",
|
|
208
|
+
"await",
|
|
209
|
+
"function",
|
|
210
|
+
"catch",
|
|
211
|
+
"typeof",
|
|
212
|
+
"new",
|
|
213
|
+
"yield"
|
|
214
|
+
]);
|
|
215
|
+
/**
|
|
216
|
+
* Where inside a hot function its time lands, when one line holds the biggest share and it is not
|
|
217
|
+
* the function's own first line: that line, its code, and the function it calls there. Once V8
|
|
218
|
+
* inlines a callee its time is the caller's own — a load "burning" 85 ms is often one call in it
|
|
219
|
+
* (measured on /inferno: newestReview inlined into load, named in one run of six). No regex.
|
|
220
|
+
*/
|
|
221
|
+
function hot_line_of(f, app_fns) {
|
|
222
|
+
const top = f.lines?.[0];
|
|
223
|
+
if (!top || top.line === f.line || top.ms < f.self_ms * .4 || !f.src) return null;
|
|
224
|
+
const text = f.src.lines[top.line - f.src.start];
|
|
225
|
+
if (text === void 0) return null;
|
|
226
|
+
const code = text.trim();
|
|
227
|
+
if (!code) return null;
|
|
228
|
+
let calls;
|
|
229
|
+
let start = -1;
|
|
230
|
+
for (let i = 0; i < code.length; i++) {
|
|
231
|
+
const c = code.charCodeAt(i);
|
|
232
|
+
if (c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c === 36) {
|
|
233
|
+
if (start === -1) start = i;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (c === 40 && start !== -1) {
|
|
237
|
+
const name = code.slice(start, i);
|
|
238
|
+
const plain = (start === 0 || code.charCodeAt(start - 1) !== 46) && name.charCodeAt(0) >= 97 && name.charCodeAt(0) <= 122;
|
|
239
|
+
if (!CALL_KEYWORDS.has(name) && name !== f.name && (app_fns.has(name) || plain)) {
|
|
240
|
+
calls = name;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
start = -1;
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
line: top.line,
|
|
248
|
+
ms: top.ms,
|
|
249
|
+
code: code.length > 100 ? code.slice(0, 99) + "…" : code,
|
|
250
|
+
...calls ? { calls } : {}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** A render that mostly waited: on its timeline, waiting (calls, and what no hook saw) is 60% of a
|
|
254
|
+
* render of 20 ms or more; with no timeline, the window's CPU under a quarter (the old reading). */
|
|
255
|
+
function mostly_waited(tl, busy_pct) {
|
|
256
|
+
if (!tl) return busy_pct < 25;
|
|
257
|
+
return tl.window_ms >= 20 && tl.wait_ms + tl.gap_ms >= tl.window_ms * .6;
|
|
258
|
+
}
|
|
259
|
+
/** "most of it" only when it is (over half); the biggest of several smaller parts otherwise */
|
|
260
|
+
const share_word = (part, whole) => whole > 0 && part > whole * .5 ? "most of it" : "the biggest part";
|
|
261
|
+
/** Three significant figures — for costs that can be nanoseconds (a getter called five million
|
|
262
|
+
* times) where a fixed decimal rounds the whole signal to zero. */
|
|
263
|
+
const sig3 = (n) => n === 0 || !Number.isFinite(n) ? 0 : Number(n.toPrecision(3));
|
|
264
|
+
/** ms per call: `null` when the call count is unknown (an anonymous function coverage cannot
|
|
265
|
+
* count) — dividing by one there would report the whole total as the cost of a single call. */
|
|
266
|
+
const per_call = (total_ms, calls) => calls ? sig3(total_ms / calls) : null;
|
|
267
|
+
const fmt_kb = (bytes) => bytes < 1024 && bytes > 0 ? `${bytes} B` : `${Math.round(bytes / 1024)} KB`;
|
|
268
|
+
/** page mode renders the page N times; per-render figures divide by it */
|
|
269
|
+
const runs_of = (meta) => meta.trigger === "page" ? Math.max(meta.runs?.length ?? 1, 1) : 1;
|
|
270
|
+
/** A URL path with its variable segments (numbers, uuids, hashes, long tokens) folded to `:id`. */
|
|
271
|
+
function path_template(url) {
|
|
272
|
+
try {
|
|
273
|
+
const u = new URL(url);
|
|
274
|
+
const tpl = u.pathname.split("/").map((seg) => /^\d+$/.test(seg) || /^[A-Za-z]{0,4}[-_]?\d+$/.test(seg) || /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(seg) || /^[0-9a-f]{16,}$/i.test(seg) || seg.length > 20 && /\d/.test(seg) ? ":id" : seg).join("/");
|
|
275
|
+
return {
|
|
276
|
+
host: u.host,
|
|
277
|
+
tpl
|
|
278
|
+
};
|
|
279
|
+
} catch {
|
|
280
|
+
return {
|
|
281
|
+
host: "",
|
|
282
|
+
tpl: url
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** N+1 shapes: the same endpoint hit once per item. */
|
|
287
|
+
function n_plus_one(net, min = 5) {
|
|
288
|
+
const groups = /* @__PURE__ */ new Map();
|
|
289
|
+
for (const c of net) {
|
|
290
|
+
if (c.ms < 0) continue;
|
|
291
|
+
const { host, tpl } = path_template(c.url);
|
|
292
|
+
if (!tpl.includes(":id") && !c.url.includes("?")) continue;
|
|
293
|
+
const key = c.method + " " + host + tpl;
|
|
294
|
+
const g = groups.get(key) ?? {
|
|
295
|
+
host,
|
|
296
|
+
tpl,
|
|
297
|
+
count: 0,
|
|
298
|
+
ms: 0
|
|
299
|
+
};
|
|
300
|
+
g.count++;
|
|
301
|
+
g.ms += c.ms + (c.body_ms ?? 0);
|
|
302
|
+
groups.set(key, g);
|
|
303
|
+
}
|
|
304
|
+
return [...groups.values()].filter((g) => g.count >= min).sort((a, b) => b.count - a.count).map((g) => ({
|
|
305
|
+
...g,
|
|
306
|
+
ms: Math.round(g.ms * 100) / 100
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
/** The plain-language bottleneck read, as structured data. The HTML verdict and
|
|
310
|
+
* the JSON report both render from this, so they never drift. */
|
|
311
|
+
/** THE PAGE'S WEIGHT, as findings: the JS it runs at start (and what the biggest files hold), what
|
|
312
|
+
* blocks the first paint, the code kept for later. From page-assets.ts — the score reads the same. */
|
|
313
|
+
/** The page's own origin, from its weighed files: its islands' (always same-origin), else its first
|
|
314
|
+
* script's. '' when nothing was weighed. */
|
|
315
|
+
function page_origin(pa) {
|
|
316
|
+
if (!pa) return "";
|
|
317
|
+
for (const via of [
|
|
318
|
+
"island",
|
|
319
|
+
"script",
|
|
320
|
+
"modulepreload"
|
|
321
|
+
]) for (const a of pa.assets) if (a.via === via) try {
|
|
322
|
+
return new URL(a.url).origin;
|
|
323
|
+
} catch {}
|
|
324
|
+
return "";
|
|
325
|
+
}
|
|
326
|
+
/** Main-thread ms per host from the browser CPU analysis (functions by their script URL). */
|
|
327
|
+
function cpu_by_host(extras) {
|
|
328
|
+
const a = extras.client_cpu?.analysis;
|
|
329
|
+
if (!a) return null;
|
|
330
|
+
const m = /* @__PURE__ */ new Map();
|
|
331
|
+
for (const f of a.functions) {
|
|
332
|
+
if (!f.path.startsWith("http")) continue;
|
|
333
|
+
let host = "";
|
|
334
|
+
try {
|
|
335
|
+
host = new URL(f.path).host;
|
|
336
|
+
} catch {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
m.set(host, (m.get(host) ?? 0) + f.self_ms);
|
|
340
|
+
}
|
|
341
|
+
return m;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* THE PAGE'S JS AT START, as the score and the findings both read it: the files weighed from the
|
|
345
|
+
* HTML and their imports, plus the scripts the profiler user's own browser loaded at runtime that
|
|
346
|
+
* nothing in the HTML names (runtime_scripts — a CDN library fetching its parts, a runtime's
|
|
347
|
+
* on-demand chunk). Without the second half, a page that loads its JS from a script weighed light.
|
|
348
|
+
*/
|
|
349
|
+
function start_js(extras) {
|
|
350
|
+
const pa = extras.assets;
|
|
351
|
+
if (!pa) return null;
|
|
352
|
+
let runtime = null;
|
|
353
|
+
let woke_early = null;
|
|
354
|
+
let runtime_parts = null;
|
|
355
|
+
const v = extras.visit;
|
|
356
|
+
if (v?.resources.length) {
|
|
357
|
+
const until = (v.nav.load ?? v.nav.res_end) + 3e3;
|
|
358
|
+
const loaded_by = v.nav.load ?? v.nav.dcl ?? v.nav.res_end;
|
|
359
|
+
const at_start = new Set(v.resources.filter((r) => r.type === "script" && r.start <= loaded_by).map((r) => r.url));
|
|
360
|
+
for (const a of pa.assets) if (a.lazy && a.kind === "script" && at_start.has(a.url)) {
|
|
361
|
+
const b = a.phase ? runtime_parts ??= {
|
|
362
|
+
bytes: 0,
|
|
363
|
+
wire: 0,
|
|
364
|
+
files: 0
|
|
365
|
+
} : woke_early ??= {
|
|
366
|
+
bytes: 0,
|
|
367
|
+
wire: 0,
|
|
368
|
+
files: 0
|
|
369
|
+
};
|
|
370
|
+
b.bytes += a.bytes;
|
|
371
|
+
b.wire += a.wire;
|
|
372
|
+
b.files++;
|
|
373
|
+
}
|
|
374
|
+
runtime = runtime_scripts(pa.assets, v.resources, page_origin(pa), until);
|
|
375
|
+
}
|
|
376
|
+
const t = pa.totals;
|
|
377
|
+
return {
|
|
378
|
+
js: t.js + (runtime?.bytes ?? 0) + (woke_early?.bytes ?? 0) + (runtime_parts?.bytes ?? 0),
|
|
379
|
+
js_wire: t.js_wire + (runtime?.wire ?? 0) + (woke_early?.wire ?? 0) + (runtime_parts?.wire ?? 0),
|
|
380
|
+
js_files: t.js_files + (runtime?.files ?? 0) + (woke_early?.files ?? 0) + (runtime_parts?.files ?? 0),
|
|
381
|
+
runtime,
|
|
382
|
+
woke_early,
|
|
383
|
+
runtime_parts
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/** The modules the build shipped twice whose copies are both in files this page loads (its weighed
|
|
387
|
+
* files and its visit's scripts), each with the bytes the second copy costs; the costliest first. */
|
|
388
|
+
function page_duplicates(extras) {
|
|
389
|
+
if (!extras.dupes?.length || !extras.assets) return [];
|
|
390
|
+
const path_of = (u) => {
|
|
391
|
+
try {
|
|
392
|
+
return new URL(u).pathname;
|
|
393
|
+
} catch {
|
|
394
|
+
return u;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
const loaded = /* @__PURE__ */ new Set();
|
|
398
|
+
for (const a of extras.assets.assets) loaded.add(path_of(a.url));
|
|
399
|
+
for (const r of extras.visit?.resources ?? []) if (r.type === "script") loaded.add(path_of(r.url));
|
|
400
|
+
const here = (file) => {
|
|
401
|
+
for (const p of loaded) if (p.endsWith(file)) return true;
|
|
402
|
+
return false;
|
|
403
|
+
};
|
|
404
|
+
return extras.dupes.map((d) => ({
|
|
405
|
+
name: d.name,
|
|
406
|
+
copies: d.copies.filter((c) => here(c.file))
|
|
407
|
+
})).filter((d) => d.copies.length >= 2).map((d) => ({
|
|
408
|
+
...d,
|
|
409
|
+
extra: d.copies.reduce((s, c) => s + c.bytes, 0) - Math.max(...d.copies.map((c) => c.bytes))
|
|
410
|
+
})).sort((a, b) => b.extra - a.extra);
|
|
411
|
+
}
|
|
412
|
+
/** A visit that loaded with the devtools dock open and recorded its first paint right after the
|
|
413
|
+
* dock began to show (the same line as the Page tab's dock-in-paint): its paints may be the dock's. */
|
|
414
|
+
function dock_skewed(v) {
|
|
415
|
+
const fcp = v?.paints?.fcp;
|
|
416
|
+
const at = v?.dock_open_at;
|
|
417
|
+
return typeof fcp === "number" && typeof at === "number" && fcp >= at && fcp - at <= 250;
|
|
418
|
+
}
|
|
419
|
+
/** What a browser hid, from what the report knows itself. Safari reports each of another origin's
|
|
420
|
+
* files as 0 bytes (Timing-Allow-Origin or not): the report weighed the files the page names and
|
|
421
|
+
* their imports, and a visit from another engine measured what scripts loaded — a hidden size takes
|
|
422
|
+
* the weight, marked. And only Chromium says which files held the first paint: for another engine,
|
|
423
|
+
* the files the HTML makes blocking (a head stylesheet, a classic sync script) are marked so. And a
|
|
424
|
+
* font downloaded again takes the Vary header the weighing read (the cause, in Safari, with Origin). */
|
|
425
|
+
function with_hidden_filled(v, origin, assets, other) {
|
|
426
|
+
if (!origin) return v;
|
|
427
|
+
const known = /* @__PURE__ */ new Map();
|
|
428
|
+
for (const r of other?.resources ?? []) if (r.size) known.set(r.url, {
|
|
429
|
+
size: r.size,
|
|
430
|
+
transfer: r.transfer ?? 0
|
|
431
|
+
});
|
|
432
|
+
for (const a of assets?.assets ?? []) if (a.bytes) known.set(a.url, {
|
|
433
|
+
size: a.bytes,
|
|
434
|
+
transfer: a.wire
|
|
435
|
+
});
|
|
436
|
+
const engine = engine_of(v.ua);
|
|
437
|
+
const blocking = engine && engine !== "chromium" ? new Set((assets?.assets ?? []).filter((a) => a.blocking).map((a) => a.url)) : null;
|
|
438
|
+
if (!known.size && !blocking?.size) return v;
|
|
439
|
+
let filled = false;
|
|
440
|
+
const resources = v.resources.map((r) => {
|
|
441
|
+
let out = r;
|
|
442
|
+
if (blocking?.has(r.url) && !r.blocking) out = {
|
|
443
|
+
...out,
|
|
444
|
+
blocking: true
|
|
445
|
+
};
|
|
446
|
+
const w = r.size || r.url.startsWith(origin + "/") ? void 0 : known.get(r.url);
|
|
447
|
+
if (w) out = {
|
|
448
|
+
...out,
|
|
449
|
+
size: w.size,
|
|
450
|
+
transfer: r.transfer || w.transfer,
|
|
451
|
+
weighed: true
|
|
452
|
+
};
|
|
453
|
+
if (out !== r) filled = true;
|
|
454
|
+
return out;
|
|
455
|
+
});
|
|
456
|
+
const vary = new Map((assets?.assets ?? []).filter((a) => a.kind === "font" && a.vary).map((a) => [a.url, a.vary]));
|
|
457
|
+
const misses = vary.size ? v.preload_misses?.map((m) => m.type === "font" && !m.vary && vary.has(m.url) ? {
|
|
458
|
+
...m,
|
|
459
|
+
vary: vary.get(m.url)
|
|
460
|
+
} : m) : void 0;
|
|
461
|
+
const misses_filled = !!misses?.some((m, i) => m !== v.preload_misses[i]);
|
|
462
|
+
return filled || misses_filled ? {
|
|
463
|
+
...v,
|
|
464
|
+
resources,
|
|
465
|
+
...misses_filled ? { preload_misses: misses } : {}
|
|
466
|
+
} : v;
|
|
467
|
+
}
|
|
468
|
+
/** browser findings whose answer is the screen's: below its first screen, hidden on it, images it
|
|
469
|
+
* shows smaller or lower down */
|
|
470
|
+
const SCREEN_CODES = /* @__PURE__ */ new Set([
|
|
471
|
+
"eager-offscreen",
|
|
472
|
+
"eager-hidden",
|
|
473
|
+
"images-eager-below",
|
|
474
|
+
"image-oversized"
|
|
475
|
+
]);
|
|
476
|
+
const IN_BROWSER = "In the browser: ";
|
|
477
|
+
const screen_of = (v) => v?.viewport ? `${v.viewport[0]}×${v.viewport[1]}` : null;
|
|
478
|
+
/** A remote function's name from its call's address (`/<appDir>/remote/<file hash>/<name>/<argument>`),
|
|
479
|
+
* or the address when it is not one. */
|
|
480
|
+
function remote_function_name(path) {
|
|
481
|
+
const parts = path.split("/");
|
|
482
|
+
const at = parts.lastIndexOf("remote", parts.length - 3);
|
|
483
|
+
return at > 0 && parts[at + 2] ? parts[at + 2] : path;
|
|
484
|
+
}
|
|
485
|
+
/** a path group is a warning from this much of one render (and 15% of its busy time): under it, a note */
|
|
486
|
+
const PATH_WARN_MS = 3;
|
|
487
|
+
/** findings about a server render — its CPU, components, calls, memory — that mean nothing when the
|
|
488
|
+
* build's file answered (no render ran: `run_static`) */
|
|
489
|
+
const RENDER_ONLY = /* @__PURE__ */ new Set([
|
|
490
|
+
"low-confidence",
|
|
491
|
+
"phases",
|
|
492
|
+
"top-cpu",
|
|
493
|
+
"top-component",
|
|
494
|
+
"component-heavy",
|
|
495
|
+
"component-repeat",
|
|
496
|
+
"hot-function",
|
|
497
|
+
"cold-start",
|
|
498
|
+
"cold-instance",
|
|
499
|
+
"slower-each-run",
|
|
500
|
+
"component-cold-run",
|
|
501
|
+
"component-variance",
|
|
502
|
+
"sequential-network",
|
|
503
|
+
"mostly-waiting",
|
|
504
|
+
"slow-upstream",
|
|
505
|
+
"n-plus-one",
|
|
506
|
+
"gc-heavy",
|
|
507
|
+
"gc-pause",
|
|
508
|
+
"gc-cause",
|
|
509
|
+
"alloc-burst",
|
|
510
|
+
"path-group",
|
|
511
|
+
"serialization",
|
|
512
|
+
"markup-heavy",
|
|
513
|
+
"logic-heavy",
|
|
514
|
+
"hot-list",
|
|
515
|
+
"retained-per-render",
|
|
516
|
+
"mem-growth",
|
|
517
|
+
"span-slow",
|
|
518
|
+
"span-repeat",
|
|
519
|
+
"memo-candidate",
|
|
520
|
+
"deopt",
|
|
521
|
+
"sync-io",
|
|
522
|
+
"billed-wait",
|
|
523
|
+
"sequential-awaits",
|
|
524
|
+
"unseen-wait",
|
|
525
|
+
"window-requests"
|
|
526
|
+
]);
|
|
527
|
+
function page_weight_findings(meta, extras, info, warn) {
|
|
528
|
+
const pa = extras.assets;
|
|
529
|
+
if (!pa) {
|
|
530
|
+
if (extras.assets_missing) info("page-unweighed", `The page's files were not weighed: ${extras.assets_missing}.`);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const t = {
|
|
534
|
+
...pa.totals,
|
|
535
|
+
...start_js(extras)
|
|
536
|
+
};
|
|
537
|
+
const islands = island_rows_of(meta).length;
|
|
538
|
+
const named = pa.assets.filter((x) => x.kind === "script" && !x.lazy).slice(0, 3).map((x) => `${asset_name(x.url)} ${fmt_bytes(x.bytes)}${x.contains?.length ? ` (${x.contains.slice(0, 4).join(", ")})` : ""}`);
|
|
539
|
+
const holes_answered = new Set((extras.visit?.holes_answered ?? []).map((h) => h.id)).size;
|
|
540
|
+
const by_holes = !!t.runtime?.island_entries && holes_answered > 0;
|
|
541
|
+
if (t.runtime && t.runtime.bytes >= 10240) {
|
|
542
|
+
const rt = t.runtime;
|
|
543
|
+
const own = rt.by.find((b) => b.who === "this app");
|
|
544
|
+
const others = rt.by.filter((b) => b.who !== "this app");
|
|
545
|
+
(rt.bytes >= 51200 ? warn : info)("runtime-scripts", by_holes && own ? `Your browser also loaded ${fmt_bytes(rt.bytes)} of JS in ${rt.files} file${rt.files === 1 ? "" : "s"} that the page's HTML and its imports never name: ${fmt_bytes(own.bytes)} (${own.files}) is this app's, the code of ${rt.island_entries} island${rt.island_entries === 1 ? "" : "s"} that arrived inside the page's holes (${holes_answered} answered) — no HTML can name what a hole will hold${others.length ? `; other scripts fetched the rest at runtime — ${others.slice(0, 3).map((b) => `${b.who} ${fmt_bytes(b.bytes)} (${b.files})`).join(", ")}` : ""}. They count in the page's JS.` : `Your browser also loaded ${fmt_bytes(rt.bytes)} of JS in ${rt.files} file${rt.files === 1 ? "" : "s"} that the page's HTML and its imports never name: other scripts fetched ${rt.files === 1 ? "it" : "them"} at runtime — ${rt.by.slice(0, 3).map((b) => `${b.who} ${fmt_bytes(b.bytes)} (${b.files})`).join(", ")}. They count in the page's JS.`, { fix: by_holes && own ? `The islands a hole holds wake as it lands: fine if they are needed at once. Otherwise give them a later wake inside the hole (\`wake: 'visible'\` or \`'interaction'\`), or make the parts that never change lakes.${others.length ? " A library that loads its own parts costs more than its first file: load it where it is used, or self-host the parts it needs." : ""}` : "A library that loads its own parts (a component CDN, a tag manager) costs more than its first file: load it where it is used, or self-host the parts it needs." });
|
|
546
|
+
}
|
|
547
|
+
{
|
|
548
|
+
const twice = page_duplicates(extras);
|
|
549
|
+
const extra = twice.reduce((s, d) => s + d.extra, 0);
|
|
550
|
+
if (twice.length && extra >= 2048) {
|
|
551
|
+
const top = twice[0];
|
|
552
|
+
(extra >= 10240 ? warn : info)("duplicate-module", `${twice.length === 1 ? `Two copies of ${top.name} ship` : `${twice.length} modules ship twice`} on this page${twice.length === 1 ? "" : ` (the heaviest, ${top.name})`}: in ${top.copies.map((c) => `${asset_name(c.file)} (${fmt_bytes(c.bytes)}${c.from ? `, from ${c.from}` : ""})`).join(" and ")}${twice.length > 1 ? `, ${fmt_bytes(extra)} loaded twice in all` : ""}. One module reached by two paths — a package's source and its build, or two of its versions — so the browser downloads, parses and runs it twice.`, { fix: "Make every import reach one copy: import the package by its public entry everywhere (not a path into its src or dist), and keep one version installed (`pnpm dedupe`, or `resolve.dedupe` in the Vite config)." });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (t.js >= 153600) (t.js >= 307200 ? warn : info)("js-at-start", `The browser runs ${fmt_bytes(t.js)} of JS in ${t.js_files} files to start this page (${fmt_bytes(t.js_wire)} on the wire${t.runtime ? `, ${fmt_bytes(t.runtime.bytes)} of it loaded at runtime${by_holes ? ", mostly islands inside its holes" : " by other scripts"}` : ""}${t.woke_early ? `, ${fmt_bytes(t.woke_early.bytes)} of it islands that wake when visible and were on your first screen` : ""}${t.runtime_parts ? `, ${fmt_bytes(t.runtime_parts.bytes)} of it the ogygia runtime's parts it loaded as the islands woke` : ""})${t.lazy_js - (t.woke_early?.bytes ?? 0) - (t.runtime_parts?.bytes ?? 0) > 0 ? `, and ${fmt_bytes(t.lazy_js - (t.woke_early?.bytes ?? 0) - (t.runtime_parts?.bytes ?? 0))} more loads only when an island needs it` : ""}. The biggest: ${named.join("; ")}.`, { fix: islands ? "Wake the heaviest islands when visible or on interaction, move heavy imports server-side, or make static subtrees lakes." : "This page hydrates as a whole (no islands): render it csr=false and make only its interactive parts islands, so their code loads when they need it." });
|
|
556
|
+
else if (t.lazy_js >= 51200) info("js-lazy", `${fmt_bytes(t.lazy_js)} of island JS waits until it is needed: the page starts with ${fmt_bytes(t.js)}.`);
|
|
557
|
+
if (t.blocking_count >= 3 || t.blocking >= 102400) {
|
|
558
|
+
const files = pa.assets.filter((x) => x.blocking).slice(0, 4).map((x) => `${asset_name(x.url)} ${fmt_bytes(x.bytes)}`);
|
|
559
|
+
const tune = inline_threshold_tune(pa.assets.filter((x) => x.blocking));
|
|
560
|
+
warn("render-blocking", `${t.blocking_count} files (${fmt_bytes(t.blocking)}) must arrive before anything paints: ${files.join(", ")}.`, { fix: tune ? `Inline the small stylesheets: with \`kit: { inlineStyleThreshold: ${tune.threshold} }\` in svelte.config.js, ${tune.files} of these sheets (${fmt_bytes(tune.bytes)}) arrive inside the HTML, and the first paint waits on ${t.blocking_count - tune.files} file${t.blocking_count - tune.files === 1 ? "" : "s"} instead of ${t.blocking_count}. Merge or trim the rest; load scripts as modules or with defer.` : "Inline the small stylesheets, merge the rest, load scripts as modules or with defer." });
|
|
561
|
+
}
|
|
562
|
+
if (pa.unscoped?.length) {
|
|
563
|
+
const f = unscoped_finding(pa.unscoped);
|
|
564
|
+
warn(f.code, f.message, { fix: f.fix });
|
|
565
|
+
}
|
|
566
|
+
if (pa.missed.length) {
|
|
567
|
+
const why = /* @__PURE__ */ new Map();
|
|
568
|
+
for (const m of pa.missed_why ?? []) {
|
|
569
|
+
const k = m.why.startsWith("status ") || m.why.startsWith("error") ? m.why.split(":")[0] : m.why;
|
|
570
|
+
why.set(k, (why.get(k) ?? 0) + 1);
|
|
571
|
+
}
|
|
572
|
+
const reasons = [...why].map(([k, n]) => `${n} ${k}`).join(", ");
|
|
573
|
+
info("assets-missed", `${pa.missed.length} file(s) the page loads could not be weighed${reasons ? ` (${reasons})` : " (a 404, a timeout, or the time budget)"}: ${pa.missed.slice(0, 3).map(asset_name).join(", ")}.${why.has("budget") || why.has("timeout") ? " Profile with `weigh=full` for totals that hold run to run." : ""}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function derive_findings(a, meta, extras) {
|
|
577
|
+
const out = [];
|
|
578
|
+
const info = (code, message, extra = {}) => out.push({
|
|
579
|
+
severity: "info",
|
|
580
|
+
code,
|
|
581
|
+
message,
|
|
582
|
+
...extra
|
|
583
|
+
});
|
|
584
|
+
const warn = (code, message, extra = {}) => out.push({
|
|
585
|
+
severity: "warn",
|
|
586
|
+
code,
|
|
587
|
+
message,
|
|
588
|
+
...extra
|
|
589
|
+
});
|
|
590
|
+
const R = runs_of(meta);
|
|
591
|
+
const pr = (ms) => ms / R;
|
|
592
|
+
const per_r = R > 1 ? " per render" : "";
|
|
593
|
+
const busy_pct = a.duration_ms > 0 ? a.busy_ms / a.duration_ms * 100 : 0;
|
|
594
|
+
const net = extras.net.filter((c) => c.ms >= 0);
|
|
595
|
+
const net_total = net.reduce((s, c) => s + c.ms + (c.body_ms ?? 0), 0);
|
|
596
|
+
const seq = sequential_ms(extras.net);
|
|
597
|
+
const net_errors = extras.net.filter((c) => c.error).length;
|
|
598
|
+
const tl = a.timeline;
|
|
599
|
+
info("summary", `Over ${fmt_ms(meta.duration_ms)} ms the CPU was busy ${busy_pct.toFixed(0)}%` + (net.length ? ` and ${net.length} outbound network calls took ${fmt_ms(net_total)} ms combined.` : "."));
|
|
600
|
+
if (meta.trigger === "page") {
|
|
601
|
+
if (meta.redirected_from && meta.page) info("redirected", meta.redirect_stub ? `Profiled ${meta.page}: ${meta.redirected_from} is a redirect the build wrote as a page (a meta refresh, answered 200, since a prerendered route has no server to send a 3xx), and its visitors land on ${meta.page}.` : `Profiled ${meta.page} — followed a redirect from ${meta.redirected_from}.`);
|
|
602
|
+
const status = meta.run_status ?? 200;
|
|
603
|
+
const bytes = meta.run_bytes ?? 0;
|
|
604
|
+
if (status >= 300) warn("not-a-render", `The profiled renders returned HTTP ${status}, not a page. This is almost always an unfollowed redirect or an error route — the numbers below are not your page. Check the path.`);
|
|
605
|
+
else if (bytes > 0 && bytes < 512) warn("not-a-render", `Each render returned only ${bytes} bytes — too small to be the real page (an error, an empty shell, or a cached stub). The profile below is not representative.`);
|
|
606
|
+
else if (a.components.length === 0 && a.sample_count < 200) {
|
|
607
|
+
const runs = [...meta.runs ?? []].sort((x, y) => x - y);
|
|
608
|
+
const ms = runs.length ? runs[runs.length >> 1] : void 0;
|
|
609
|
+
info("low-confidence", `${ms !== void 0 ? `This page renders in about ${fmt_ms(ms)} ms: o` : "O"}nly ${a.sample_count} CPU sample${a.sample_count === 1 ? "" : "s"} and no component took measurable time — there is little here to make faster, and too little for the numbers below to be exact. (Expected a heavier page? A cached or prerendered answer looks like this too: check the URL.)`);
|
|
610
|
+
}
|
|
611
|
+
if (meta.warmup_ms !== void 0 && meta.runs?.length) {
|
|
612
|
+
const median = [...meta.runs].sort((x, y) => x - y)[Math.floor(meta.runs.length / 2)];
|
|
613
|
+
if (median > 0 && meta.warmup_ms > median * 8 && meta.warmup_ms > 200) info("cached-after-first", `The first (warm-up) render took ${fmt_ms(meta.warmup_ms)} ms but the timed runs averaged ~${fmt_ms(median)} ms — the app appears to cache this page after the first render, so the profile reflects cache hits, not the ${fmt_ms(meta.warmup_ms)} ms first paint.`);
|
|
614
|
+
}
|
|
615
|
+
if (meta.budget_note) info("budget", meta.budget_note);
|
|
616
|
+
if (meta.heap_guard?.skipped.length) warn("heap-near-limit", `The server's heap was nearly full (${meta.heap_guard.used_mb} of ${meta.heap_guard.limit_mb} MB), so the profiler left out ${meta.heap_guard.skipped.join(", ")}: one more render of this page could have run it out of memory and ended the process. What it keeps per render is the cause.`, { fix: "Fix what a render keeps alive (the Kept per render pattern names the line), and restart the server before profiling again." });
|
|
617
|
+
}
|
|
618
|
+
if (tl) {
|
|
619
|
+
const parts = tl.phases.filter((p) => p.cpu_ms + p.wait_ms >= tl.window_ms * .03).map((p) => `${PHASE_LABEL[p.phase]} ${fmt_ms(p.cpu_ms)} ms` + (p.wait_ms >= .5 ? ` + ${fmt_ms(p.wait_ms)} ms waiting` : "") + (p.top?.length && p.cpu_ms >= tl.window_ms * .1 ? ` (${p.top.slice(0, 2).map((o) => `${o.label} ${fmt_ms(o.ms)} ms`).join(", ")})` : ""));
|
|
620
|
+
if (parts.length) info("phases", `Of the ${fmt_ms(tl.window_ms)} ms render: ${parts.join(" · ")}` + (tl.gap_ms >= tl.window_ms * .05 ? ` · ${fmt_ms(tl.gap_ms)} ms unaccounted` : "") + ".");
|
|
621
|
+
if (meta.lambda && tl.wait_ms >= tl.window_ms * .3 && tl.wait_ms >= 50) info("billed-wait", `On AWS Lambda a request is billed for its whole duration: ${fmt_ms(tl.window_ms)} ms here, and ${fmt_ms(tl.wait_ms)} ms of it (${fmt_pct(tl.wait_ms, tl.window_ms)}) is waiting on calls, not computing. Every millisecond of waiting cut is billed time cut, the same as CPU.`, { fix: "Start independent calls together, cache what repeats across requests, and fetch less per render: the wait patterns above name the lines." });
|
|
622
|
+
const group = [...tl.parallelizable].sort((x, y) => y.save_ms - x.save_ms)[0];
|
|
623
|
+
if (group && group.save_ms >= Math.max(5, tl.window_ms * .05)) warn("sequential-awaits", `${group.calls.length} calls ran one after another for ${fmt_ms(group.ms)} ms: ${group.calls.join(", ")}. Started together they would cost ${fmt_ms(group.ms - group.save_ms)} ms — about ${fmt_ms(group.save_ms)} ms saved.`, { fix: "Start them together (Promise.all) when they are independent; a call that needs an earlier result stays sequential." });
|
|
624
|
+
if (tl.gap_ms > tl.window_ms * .3 && tl.gap_ms > 20) warn("unseen-wait", `${fmt_ms(tl.gap_ms)} ms (${fmt_pct(tl.gap_ms, tl.window_ms)}) of the render waited on something the recorder cannot see — a database driver over its own socket pool, a worker, a queue.`, { fix: "Wrap that client in performance.measure() spans; they show under User timings and on the timeline next time." });
|
|
625
|
+
}
|
|
626
|
+
for (const g of n_plus_one(net).slice(0, 2)) warn("n-plus-one", `${g.count} calls to ${g.host}${g.tpl} — one per item, ${fmt_ms(g.ms)} ms together.`, { fix: "Batch them: one request with the ids (or a bulk endpoint), or fetch the list with its children included." });
|
|
627
|
+
if (meta.trigger === "window") {
|
|
628
|
+
const reqs = meta.requests.filter((r) => !r.internal && r.ms >= 0);
|
|
629
|
+
if (reqs.length) {
|
|
630
|
+
const slow = [...reqs].sort((x, y) => y.ms - x.ms).slice(0, 3);
|
|
631
|
+
const split = (r) => {
|
|
632
|
+
const wait = Math.max(0, r.ms - r.cpu_ms);
|
|
633
|
+
return `${r.method} ${r.path.split("?")[0]} ${fmt_ms(r.ms)} ms (${fmt_ms(r.cpu_ms)} ms CPU, ${fmt_ms(wait)} ms waiting${r.net_count ? `, ${fmt_ms(r.net_ms)} ms of it on ${r.net_count} outbound call${r.net_count === 1 ? "" : "s"}` : ""})`;
|
|
634
|
+
};
|
|
635
|
+
const overlapped = reqs.some((r) => r.inflight > 0);
|
|
636
|
+
(slow[0].ms >= 1e3 ? warn : info)("window-requests", `${reqs.length} request${reqs.length === 1 ? "" : "s"} in the window; the slowest: ${slow.map(split).join(" · ")}.` + (overlapped ? " Some ran at the same time: their CPU is the process’s over each one, so it overlaps." : ""), slow[0].ms >= 1e3 ? { fix: slow[0].cpu_ms >= slow[0].ms * .5 ? "It is computing: profile that page on its own (page mode) for the lines." : "It is waiting: the network table and the upstream split say on what." } : {});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (meta.trigger !== "window" && !tl?.parallelizable.length && net.length >= 2 && seq > meta.duration_ms * .5 && busy_pct < 60) warn("sequential-network", `Network calls ran back-to-back for ${fmt_ms(seq)} ms of the window — usually sequential awaits.`, { fix: "Start independent calls together with Promise.all." });
|
|
640
|
+
else if (meta.trigger !== "window" && (meta.run_status ?? 200) < 300 && mostly_waited(tl, busy_pct) && (net.length || !tl || tl.wait_ms >= tl.gap_ms)) {
|
|
641
|
+
const part = tl ? `: ${fmt_ms(tl.wait_ms + tl.gap_ms)} of the ${fmt_ms(tl.window_ms)} ms render` : "";
|
|
642
|
+
info("mostly-waiting", net.length ? `Mostly waiting on the network, not computing${part}.` : `Mostly waiting, not computing${part}, and on no HTTP call: a timer, a file, or a database/socket client (the waiting table names it).`);
|
|
643
|
+
}
|
|
644
|
+
const th = top_hosts(net)[0];
|
|
645
|
+
if (th && th.total > meta.duration_ms * .2) info("slow-upstream", `Slowest upstream: ${th.host || "(unknown)"} — ${th.count} calls, ${fmt_ms(th.total)} ms.`);
|
|
646
|
+
if (net_errors) warn("network-errors", `${net_errors} network call${net_errors > 1 ? "s" : ""} failed.`);
|
|
647
|
+
const tb = a.buckets.find((b) => b.category !== "idle" && b.category !== "profiler");
|
|
648
|
+
if (tb) info("top-cpu", `Biggest CPU consumer: ${tb.key} (${fmt_ms(pr(tb.self_ms))} ms${per_r}, ${fmt_pct(tb.self_ms, a.busy_ms)} of busy time).`);
|
|
649
|
+
const by_total = [...a.components].sort((x, y) => y.total_ms - x.total_ms);
|
|
650
|
+
const is_root = (n) => /^_(?:page|layout|error)$|^Root$/.test(n);
|
|
651
|
+
const heavy = by_total.find((c) => !is_root(c.name) && c.total_ms >= a.busy_ms * .2 && c.total_ms >= 5) ?? by_total.find((c) => c.total_ms >= a.busy_ms * .2 && c.total_ms >= 5);
|
|
652
|
+
if (heavy) {
|
|
653
|
+
const n = heavy.calls ?? 0;
|
|
654
|
+
const heavy_ms = heavy.total_ms / runs_of(meta);
|
|
655
|
+
const at = {
|
|
656
|
+
anchor: `comp:${heavy.name}`,
|
|
657
|
+
file: heavy.url,
|
|
658
|
+
line: heavy.line
|
|
659
|
+
};
|
|
660
|
+
if (n >= 20) warn("component-repeat", `${heavy.name} rendered ${n} times per render — ${fmt_ms(heavy_ms)} ms, ${fmt_pct(heavy.total_ms, a.busy_ms)} of busy, ${fmt_ms(heavy_ms / n)} ms each.`, {
|
|
661
|
+
...at,
|
|
662
|
+
fix: "Render fewer: paginate or window the list, or move it below the fold into a deferred hole so the page ships without it."
|
|
663
|
+
});
|
|
664
|
+
else if (heavy_ms / Math.max(n, 1) >= 20) {
|
|
665
|
+
const inside = a.functions.find((f) => f.key !== heavy.key && f.self_ms >= heavy.total_ms * .2 && f.stacks?.[0]?.frames.some((fr) => fr.n === heavy.name));
|
|
666
|
+
warn("component-heavy", `One render of ${heavy.name} costs ${fmt_ms(heavy_ms / Math.max(n, 1))} ms (${fmt_pct(heavy.total_ms, a.busy_ms)} of busy)` + (inside ? ` — ${share_word(inside.self_ms, heavy.total_ms)} is ${inside.name} (${inside.url}:${inside.line}), ${fmt_ms(inside.self_ms / runs_of(meta))} ms.` : "."), {
|
|
667
|
+
...at,
|
|
668
|
+
fix: "Compute the expensive part once (in load, or a per-request cache) and pass the result as a prop, or render it in a deferred hole."
|
|
669
|
+
});
|
|
670
|
+
} else info("top-component", `Most expensive component: ${heavy.name} at ${fmt_ms(heavy_ms)} ms ${meta.trigger === "window" ? "across the window" : "per render"}.`, at);
|
|
671
|
+
} else if (by_total[0]) {
|
|
672
|
+
const tc = by_total[0];
|
|
673
|
+
info("top-component", `Most expensive component: ${tc.name} at ${fmt_ms(tc.total_ms / runs_of(meta))} ms ${meta.trigger === "window" ? "across the window" : "per render"}.`, {
|
|
674
|
+
anchor: `comp:${tc.name}`,
|
|
675
|
+
file: tc.url,
|
|
676
|
+
line: tc.line
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const own = a.functions.filter((f) => f.category === "app" || f.category === "dependency");
|
|
680
|
+
const hot = own[0];
|
|
681
|
+
const top3 = own.slice(0, 3).filter((f) => f.self_ms >= 5);
|
|
682
|
+
const top3_ms = top3.reduce((s, f) => s + f.self_ms, 0);
|
|
683
|
+
const single = !!hot && hot.self_ms >= a.busy_ms * .2;
|
|
684
|
+
const crowd = !!hot && !single && top3.length === 3 && hot.self_ms >= a.busy_ms * .12 && top3_ms >= a.busy_ms * .4;
|
|
685
|
+
const where = (f) => f.label ? `the function at ${f.url}:${f.line}` : `${f.name} (${f.url}:${f.line})`;
|
|
686
|
+
if (hot && hot.self_ms >= 5 && (single || crowd)) {
|
|
687
|
+
const from = hot.stacks?.[0]?.frames.find((fr) => fr.c === "component" || fr.c === "app");
|
|
688
|
+
const spot = hot_line_of(hot, new Set(a.functions.filter((f) => f.category === "app").map((f) => f.name)));
|
|
689
|
+
const close = own.slice(1, 3).filter((f) => f.self_ms >= 5 && f.self_ms >= hot.self_ms * .75);
|
|
690
|
+
const spot_lead = crowd ? `In ${hot.name}, the biggest part` : spot && share_word(spot.ms, hot.self_ms) === "most of it" ? "Most of it" : "The biggest part";
|
|
691
|
+
warn("hot-function", (crowd ? `No one function dominates, but three together burn ${fmt_ms(pr(top3_ms))} ms${per_r} (${fmt_pct(top3_ms, a.busy_ms)} of busy): ${top3.map((f) => `${where(f)} ${fmt_pct(f.self_ms, a.busy_ms)}`).join(", ")}.` : `${hot.label ? `The function at ${hot.url}:${hot.line} (\`${hot.label}\`)` : hot.name} burns ${fmt_ms(pr(hot.self_ms))} ms${per_r} (${fmt_pct(hot.self_ms, a.busy_ms)} of busy)${hot.label ? "" : ` at ${hot.url}:${hot.line}`}` + (hot.calls ? `, ${hot.calls} calls per render` : "") + (from ? `, called from ${from.n}` : "") + "." + (close.length ? ` Close behind: ${close.map((f) => `${where(f)} ${fmt_pct(f.self_ms, a.busy_ms)}`).join(", ")}.` : "")) + (spot ? ` ${spot_lead} lands on line ${spot.line}: \`${spot.code}\`` + (spot.calls ? ` — ${spot.calls} is called there, and V8 counts a function it inlined as the caller's own time: look inside ${spot.calls}.` : ".") : ""), {
|
|
692
|
+
anchor: `fn:${hot.key}`,
|
|
693
|
+
file: hot.url,
|
|
694
|
+
line: spot?.line ?? hot.line,
|
|
695
|
+
fix: hot.category === "dependency" ? `It is ${hot.pkg}'s — cache or batch what you ask of it per request, and check the call count against what the page really needs.` : "Hoist per-request work out of per-row code, precompute it in load, or cache the result across requests."
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
const ser = a.functions.filter((f) => f.pkg === "devalue" || f.name === "stringify" && f.category === "dependency");
|
|
699
|
+
const ser_ms = Math.max(0, ser.reduce((s, f) => s + f.self_ms, 0) - kit_load_serialize_ms(a));
|
|
700
|
+
if (ser_ms >= a.busy_ms * .1 && ser_ms >= 5) warn("serialization", `Serializing data took ${fmt_ms(pr(ser_ms))} ms${per_r} (${fmt_pct(ser_ms, a.busy_ms)} of busy) — the page's load data and island props on their way to the browser.`, {
|
|
701
|
+
anchor: ser[0] ? `fn:${ser[0].key}` : void 0,
|
|
702
|
+
fix: "Ship less: return from load only what the page reads, keep big blobs out of page.data, and let islands take props rather than reading $page whole."
|
|
703
|
+
});
|
|
704
|
+
const runs = meta.trigger === "page" ? Math.max(meta.runs?.length ?? 1, 1) : 1;
|
|
705
|
+
const spans = span_rows(extras.spans);
|
|
706
|
+
const window_ms = tl?.window_ms ?? meta.duration_ms / runs;
|
|
707
|
+
const in_one = meta.trigger === "window" ? "during the window" : "in one render";
|
|
708
|
+
for (const s of spans) {
|
|
709
|
+
const per_render = s.count / runs;
|
|
710
|
+
const ms_per_render = s.total_ms / runs;
|
|
711
|
+
const site = s.callers[0] ? ` (${s.callers[0]})` : "";
|
|
712
|
+
if (s.errors) warn("span-errors", `${s.name} failed ${s.errors} time${s.errors === 1 ? "" : "s"}${site}.`, { fix: "A failed span is a request that paid for work it could not use — read the error on the Spans table." });
|
|
713
|
+
if (s.open) warn("span-open", `${s.name} never ended ${s.open} time${s.open === 1 ? "" : "s"}${site} — a hung call, or a span.start() without end().`);
|
|
714
|
+
const wall_per_render = s.wall_ms / runs;
|
|
715
|
+
if (per_render >= 5 && wall_per_render >= Math.max(5, window_ms * .05)) {
|
|
716
|
+
const bk = Object.keys(s.by)[0];
|
|
717
|
+
const top = bk ? s.by[bk][0] : void 0;
|
|
718
|
+
const overlap = s.wall_ms < s.total_ms * .6;
|
|
719
|
+
const top_share = top ? [...s.by[bk]].sort((x, y) => y.share_ms - x.share_ms)[0] : void 0;
|
|
720
|
+
const by_top = overlap ? top_share && bk && !top_share.value.startsWith("(") && top_share.share_ms >= s.wall_ms * .2 ? ` The biggest part is ${bk} ${top_share.value}: ${Math.round(top_share.count / runs)} of them, ${fmt_ms(top_share.share_ms / runs)} ms of the ${fmt_ms(wall_per_render)} ms (${fmt_pct(top_share.share_ms, s.wall_ms)}).` : "" : top && bk && !top.value.startsWith("(") && top.wall_ms >= s.wall_ms * .2 ? ` Most of it is ${bk} ${top.value}: ${Math.round(top.count / runs)} of them, ${fmt_ms(top.wall_ms / runs)} ms of wall.` : "";
|
|
721
|
+
if (overlap) warn("span-repeat", `${s.name} ran ${Math.round(per_render)} times ${in_one}${site}, up to ${s.peak} at once, and together they took ${fmt_ms(wall_per_render)} ms. Each one's part of that is about ${fmt_ms(s.share_p50_ms)} ms; its own start to end (${fmt_ms(s.p50_ms)} ms) is mostly waiting its turn behind the others.${by_top}`, { fix: "They already overlap, so batching gains little: the cost is each one (make it cheaper, or cache it) and how many there are (render fewer)." });
|
|
722
|
+
else warn("span-repeat", `${s.name} ran ${Math.round(per_render)} times ${in_one}${site}, ${fmt_ms(ms_per_render)} ms together, ${fmt_ms(s.p50_ms)} ms each.${by_top}`, { fix: "Once per item is the N+1 shape: batch it (one call for all the ids), or fetch the parent with its children included." });
|
|
723
|
+
} else if ((s.wall_ms < s.total_ms * .6 ? wall_per_render : ms_per_render) >= Math.max(10, window_ms * .2)) {
|
|
724
|
+
const together = s.wall_ms < s.total_ms * .6;
|
|
725
|
+
const took = together ? wall_per_render : ms_per_render;
|
|
726
|
+
warn("span-slow", `${s.name} took ${fmt_ms(took)} ms of the ${meta.trigger === "window" ? "window" : "render"}${site}${per_render > 1 ? `, over ${Math.round(per_render)} calls${together ? ` running together (${fmt_ms(ms_per_render)} ms summed)` : ""}` : ""}.`, { fix: "The profiler cannot see inside it — this is the wait to take to whoever owns that service, or to cache." });
|
|
727
|
+
}
|
|
728
|
+
if (s.cache && s.cache.miss_ms / runs >= Math.max(5, window_ms * .05)) warn("cache-misses", `${s.name}: ${Math.round(s.cache.miss / runs)} cache miss${s.cache.miss / runs >= 2 ? "es" : ""} per render cost ${fmt_ms(s.cache.miss_ms / runs)} ms (${s.cache.hit} hit${s.cache.hit === 1 ? "" : "s"} in the window).`, { fix: "A miss on every render is no cache: check the key, the TTL, or whether the cache is per-instance on a host that spins instances up per request." });
|
|
729
|
+
}
|
|
730
|
+
const etag = a.functions.find((f) => f.name === "hash" && f.pkg === "@sveltejs/kit");
|
|
731
|
+
if (etag && etag.self_ms >= a.busy_ms * .03 && etag.self_ms >= 3) {
|
|
732
|
+
const bytes = meta.run_bytes ?? meta.requests.find((r) => r.internal)?.og?.tail_bytes;
|
|
733
|
+
info("kit-etag", `Kit hashed the whole HTML for its ETag: ${fmt_ms(pr(etag.self_ms))} ms per render` + (bytes ? ` over ${fmt_kb(bytes)}` : "") + ". The `hash` function is Kit’s (`render_response` calls it on a page that does not stream), not your code, wherever the build put it.", {
|
|
734
|
+
anchor: `fn:${etag.key}`,
|
|
735
|
+
fix: "Kit skips the ETag on a streamed page: return a promise from a server load that the page awaits (it streams, and Kit sends no ETag). Otherwise only a smaller document helps — fewer bytes in the seed and props, less markup."
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const og = [...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;
|
|
739
|
+
if (og) {
|
|
740
|
+
info("ogygia-cost", `ogygia added ${fmt_ms(og.transform_ms)} ms to the render: ${og.islands} island${og.islands === 1 ? "" : "s"}` + (og.holes ? `, ${og.holes} hole${og.holes === 1 ? "" : "s"}` : "") + `, seed ${fmt_kb(og.seed_bytes)}, props ${fmt_kb(og.tail_bytes)}` + (og.remote_seed_bytes ? `, remote seed ${fmt_kb(og.remote_seed_bytes)}` : "") + ".");
|
|
741
|
+
if (og.seed_bytes > 102400) warn("seed-large", `The page seed is ${fmt_kb(og.seed_bytes)}: an island reads $page, so that much of page.data ships to the browser and is serialized on every render.`, { fix: "Read only the keys the island needs (page.data.x, not page.data) so seed shaping trims it, or pass the values as props." });
|
|
742
|
+
if (og.tail_bytes > 204800) warn("props-large", `Island props total ${fmt_kb(og.tail_bytes)} across ${og.islands} islands.`, { fix: "Pass each island the slice it renders, not the whole record; a prop that is a page.data node crosses as a reference for free." });
|
|
743
|
+
ogygia_findings(og, meta, extras, info, warn);
|
|
744
|
+
}
|
|
745
|
+
page_weight_findings(meta, extras, info, warn);
|
|
746
|
+
const cc = (meta.run_cache_control ?? "").toLowerCase();
|
|
747
|
+
if (meta.trigger === "page" && cc.split(",").some((d) => d.trim() === "no-store")) warn("bfcache-no-store", `The page answers with Cache-Control: ${meta.run_cache_control}: the browser keeps no page marked no-store in its back/forward cache, so every Back and Forward to it is another full render here (and another wait for the visitor).`, { fix: "Drop no-store from pages that hold nothing private, or use `private, no-cache` (the browser keeps the page and checks with the server before reusing it). Keep no-store for pages that must never be shown again after logout." });
|
|
748
|
+
if (meta.trigger === "page" && meta.prerendered) info("prerendered-page", meta.run_static ? "This page is a file the build wrote (prerendered): the server answered with it before the app saw the request, so no render ran — the times above are the file being served, nothing to make faster on the server. A visit to it cannot be measured either: static HTML carries no profiler tag." : `This page is prerendered (its route exports \`prerender = true\`): production serves the HTML the build wrote, so its server render costs nothing per request — the times above are what the build spends on it, once. A visit to the built file cannot be measured either: static HTML carries no profiler tag.`, { fix: "Look at what the browser does with it instead: the page weight, the islands it wakes, its JS — and the devtools Page tab on a visit, which measures any page." });
|
|
749
|
+
const els = extras.strip?.elements;
|
|
750
|
+
if (els && els.total >= DOM_LARGE && !extras.visit?.dom) {
|
|
751
|
+
const top = els.islands[0];
|
|
752
|
+
const name = (top && top.n >= els.total * .3 ? island_rows_of(meta).find((r) => r.fp === top.fp) : void 0)?.name || (top ? `the island ${top.fp.slice(0, 8)}` : "");
|
|
753
|
+
const held = top && top.n >= els.total * .3;
|
|
754
|
+
const n = (x) => x.toLocaleString("en-US");
|
|
755
|
+
warn("dom-large", `The HTML makes ${n(els.total)} elements` + (held ? `, ${n(top.n)} of them inside ${name}: an island hydrates over every element of its own, so it pays for all of them as it wakes.` : ": every element costs the browser memory and style and layout work on each change."), { fix: held ? `Render fewer at once in ${name}: page or window a long list (only what is on screen), and keep big static markup outside the island (a lake costs nothing to hydrate).` : "Render fewer at once: page or window long lists (only what is on screen), and flatten markup that nests wrappers for layout alone." });
|
|
756
|
+
}
|
|
757
|
+
const unsized = extras.visit?.images_unsized ? [] : extras.strip?.unsized_images ?? [];
|
|
758
|
+
if (unsized.length) {
|
|
759
|
+
const file = (u) => {
|
|
760
|
+
const q = u.indexOf("?");
|
|
761
|
+
const p = q === -1 ? u : u.slice(0, q);
|
|
762
|
+
return p.slice(p.lastIndexOf("/") + 1) || u;
|
|
763
|
+
};
|
|
764
|
+
const names = [...new Set(unsized.map(file))];
|
|
765
|
+
const cls = extras.vitals?.cls ?? null;
|
|
766
|
+
const moved = cls !== null && cls >= .1;
|
|
767
|
+
(moved ? warn : info)("img-unsized", `${names.length === 1 ? `An image in the HTML (${names[0]}) has` : `${unsized.length >= 8 ? "8 or more" : unsized.length} images in the HTML (${names.slice(0, 4).join(", ")}${names.length > 4 ? "…" : ""}) have`} no width and height: the browser cannot hold their room before the file arrives, so what is below moves when it does${moved ? ` — and the browser measured the page moving (CLS ${cls})` : " (unless CSS gives them a size)"}.`, { fix: "Give each `<img>` its width and height attributes (the file’s own; CSS can still scale it): the browser then holds the room from the first paint. Or give it a CSS aspect-ratio." });
|
|
768
|
+
}
|
|
769
|
+
const tail = extras.strip?.tail;
|
|
770
|
+
const held_open = tail?.keys.length ? {
|
|
771
|
+
side: "server",
|
|
772
|
+
keys: tail.keys.map((k) => ({
|
|
773
|
+
key: k.key,
|
|
774
|
+
at: k.left_ms
|
|
775
|
+
})),
|
|
776
|
+
early_ms: tail.early_ms,
|
|
777
|
+
early_bytes: tail.early_bytes,
|
|
778
|
+
late_ms: tail.late_ms,
|
|
779
|
+
late_bytes: tail.late_bytes,
|
|
780
|
+
islands: island_rows_of(meta).length
|
|
781
|
+
} : void 0;
|
|
782
|
+
if (!extras.visit && held_open) {
|
|
783
|
+
const f = explain_held_open({
|
|
784
|
+
vitals: {},
|
|
785
|
+
visit: null,
|
|
786
|
+
islands: [],
|
|
787
|
+
firsts: [],
|
|
788
|
+
shifts: [],
|
|
789
|
+
longtasks: [],
|
|
790
|
+
held_open
|
|
791
|
+
}, [], (fp) => fp);
|
|
792
|
+
if (f) warn(f.code, f.message, { fix: f.fix });
|
|
793
|
+
}
|
|
794
|
+
if (extras.visit) {
|
|
795
|
+
const origin = extras.visit.origin || page_origin(extras.assets);
|
|
796
|
+
const third = origin ? {
|
|
797
|
+
origin,
|
|
798
|
+
named: extras.assets?.assets.map((a) => a.url),
|
|
799
|
+
by_host: cpu_by_host(extras)
|
|
800
|
+
} : void 0;
|
|
801
|
+
const hole_by_id = new Map((own_requests(meta).find((r) => r.og?.hole_rows?.length)?.og?.hole_rows ?? []).map((h) => [h.id, h]));
|
|
802
|
+
const hole_name = (id, p) => {
|
|
803
|
+
const h = hole_by_id.get(id);
|
|
804
|
+
if (h) {
|
|
805
|
+
const copy = p ? h.copies?.find((c) => c.p === p) : void 0;
|
|
806
|
+
if (copy) return hole_label({
|
|
807
|
+
...h,
|
|
808
|
+
props: copy.props
|
|
809
|
+
});
|
|
810
|
+
return (h.copies?.length ?? 0) > 1 && h.name ? h.name : hole_label(h);
|
|
811
|
+
}
|
|
812
|
+
return (extras.hole_requests ?? []).find((r) => r.id === id && r.name)?.name ?? `the hole ${id}`;
|
|
813
|
+
};
|
|
814
|
+
const econ = hole_economics(meta);
|
|
815
|
+
const hole_server = (id, p) => {
|
|
816
|
+
const of_id = (extras.hole_requests ?? []).filter((r) => r.id === id);
|
|
817
|
+
const of_copy = p ? of_id.filter((r) => r.p === p) : [];
|
|
818
|
+
const own = of_copy.length ? of_copy : of_id;
|
|
819
|
+
if (own.length) return own.reduce((a, r) => a + r.ms, 0) / own.length;
|
|
820
|
+
const e = econ.get(id);
|
|
821
|
+
const n = e ? e.hit + e.miss + e.none : 0;
|
|
822
|
+
return e && n ? e.ms / n : void 0;
|
|
823
|
+
};
|
|
824
|
+
const nav_req = new Map((extras.nav_requests ?? []).map((r) => [r.t, r]));
|
|
825
|
+
const sized = with_hidden_filled(extras.visit, origin, extras.assets, extras.other_engine_visit);
|
|
826
|
+
const visit = nav_req.size && sized.navs ? {
|
|
827
|
+
...sized,
|
|
828
|
+
navs: sized.navs.map((n) => nav_req.has(n.t) ? {
|
|
829
|
+
...n,
|
|
830
|
+
on_server: nav_req.get(n.t)
|
|
831
|
+
} : n)
|
|
832
|
+
} : sized;
|
|
833
|
+
const all_rows = island_rows_of(meta);
|
|
834
|
+
const own = island_js_unique(all_rows, extras.weights);
|
|
835
|
+
const visit_rows = own ? all_rows.map((r) => own.get(r.entry) ? {
|
|
836
|
+
...r,
|
|
837
|
+
own_bytes: own.get(r.entry)
|
|
838
|
+
} : r) : all_rows;
|
|
839
|
+
const in_browser = browser_findings(browser_page_report(visit, visit_rows, extras.client_cpu?.windows, third, hole_name, hole_server, extras.interaction_cpu, !!meta.dev, held_open)).filter((f) => f.code !== "render-blocking" || !extras.assets);
|
|
840
|
+
const here = screen_of(visit);
|
|
841
|
+
const engine = browser_of(visit?.ua);
|
|
842
|
+
const who = engine && !engine.startsWith("Chrome") && !engine.startsWith("Edge") && !engine.startsWith("Opera") ? ` (${engine})` : "";
|
|
843
|
+
out.push(...in_browser.map((f) => {
|
|
844
|
+
if (!f.message.startsWith(IN_BROWSER)) return f;
|
|
845
|
+
const rest = f.message.slice(16);
|
|
846
|
+
if (here && SCREEN_CODES.has(f.code)) return {
|
|
847
|
+
...f,
|
|
848
|
+
message: `In the browser${who}, on a ${here} screen: ${rest}`
|
|
849
|
+
};
|
|
850
|
+
return who ? {
|
|
851
|
+
...f,
|
|
852
|
+
message: `In the browser${who}: ${rest}`
|
|
853
|
+
} : f;
|
|
854
|
+
}));
|
|
855
|
+
const other = extras.other_screen_visit;
|
|
856
|
+
const there = other ? screen_of(other) : null;
|
|
857
|
+
if (other && there) {
|
|
858
|
+
const ours = new Map(in_browser.filter((f) => SCREEN_CODES.has(f.code)).map((f) => [f.code, new Set(f.fps ?? [])]));
|
|
859
|
+
const theirs = browser_findings(browser_page_report(other, visit_rows, void 0, third, hole_name, hole_server, void 0, !!meta.dev)).filter((f) => {
|
|
860
|
+
if (!SCREEN_CODES.has(f.code)) return false;
|
|
861
|
+
const o = ours.get(f.code);
|
|
862
|
+
return !o || (f.fps ?? []).some((fp) => !o.has(fp));
|
|
863
|
+
});
|
|
864
|
+
if (theirs.length) out.push({
|
|
865
|
+
severity: "info",
|
|
866
|
+
code: "other-screen",
|
|
867
|
+
message: `On a ${there} screen (another visit to this page) the browser saw what this one did not: ${theirs.map((f) => f.message.slice(f.message.startsWith(IN_BROWSER) ? 16 : 0)).join(" ")}`,
|
|
868
|
+
...theirs.some((f) => f.fix) ? { fix: [...new Set(theirs.map((f) => f.fix).filter((x) => !!x))].slice(0, 2).join(" ") } : {},
|
|
869
|
+
fps: [...new Set(theirs.flatMap((f) => f.fps ?? []))]
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const eng = extras.other_engine_visit;
|
|
873
|
+
const eng_name = eng ? browser_of(eng.ua) : null;
|
|
874
|
+
if (eng && eng_name) {
|
|
875
|
+
const have = new Set(in_browser.map((f) => f.code));
|
|
876
|
+
const theirs = browser_findings(browser_page_report(eng, visit_rows, void 0, third, hole_name, hole_server, void 0, !!meta.dev)).filter((f) => !have.has(f.code) && !SCREEN_CODES.has(f.code) && f.code !== "dock-in-paint");
|
|
877
|
+
if (theirs.length) out.push({
|
|
878
|
+
severity: "info",
|
|
879
|
+
code: "other-browser",
|
|
880
|
+
message: `In ${eng_name} (another visit to this page) the browser saw what this one did not: ${theirs.map((f) => f.message.slice(f.message.startsWith(IN_BROWSER) ? 16 : 0)).join(" ")}`,
|
|
881
|
+
...theirs.some((f) => f.fix) ? { fix: [...new Set(theirs.map((f) => f.fix).filter((x) => !!x))].slice(0, 2).join(" ") } : {},
|
|
882
|
+
fps: [...new Set(theirs.flatMap((f) => f.fps ?? []))]
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
const WHAT = {
|
|
886
|
+
"layout-shift": extras.visit.shifts_from === "boxes" ? "layout shifts (a wake that moved the layout is estimated from the island's box)" : "layout shifts",
|
|
887
|
+
longtask: extras.visit.longtasks_from === "frames" ? "long tasks (its stalls are estimated from the frames it drew late)" : "long tasks",
|
|
888
|
+
event: "interaction timing",
|
|
889
|
+
"largest-contentful-paint": "the largest paint",
|
|
890
|
+
"long-animation-frame": "which script held a frame"
|
|
891
|
+
};
|
|
892
|
+
const blind = (extras.visit.unsupported ?? []).map((t) => WHAT[t]).filter(Boolean);
|
|
893
|
+
if (blind.length) info("browser-limits", `${browser_of(extras.visit.ua) ? `${browser_of(extras.visit.ua)}, the browser that visited,` : "The browser that visited"} does not report ${blind.join(", ")}: findings and score parts that need them are missing from this report, not clean. Visit the page from a Chromium browser to measure them.`);
|
|
894
|
+
}
|
|
895
|
+
kit_findings(a, meta, info, warn);
|
|
896
|
+
accuracy_findings(a, meta, extras, info, warn);
|
|
897
|
+
for (const g of (a.paths ?? []).slice(0, 3)) (g.ms >= a.busy_ms * .15 && pr(g.ms) >= PATH_WARN_MS ? warn : info)("path-group", `${g.fns.length} hot functions sit on one path under ${g.owner.name}: ${g.fns.slice(0, 4).map((f) => f.name).join(", ")}${g.fns.length > 4 ? ` and ${g.fns.length - 4} more` : ""} — ${fmt_ms(pr(g.ms))} ms together${per_r} (${fmt_pct(g.ms, a.busy_ms)} of busy).`, {
|
|
898
|
+
anchor: g.owner.category === "component" ? `comp:${g.owner.name}` : `fn:${g.owner.key}`,
|
|
899
|
+
file: g.owner.url,
|
|
900
|
+
line: g.owner.line,
|
|
901
|
+
fix: `Fix ${g.owner.name} once — call it less, cache what it computes, or move it out of the render — rather than each function on its own; the graph under "Paths to fix" shows the chain.`
|
|
902
|
+
});
|
|
903
|
+
if (a.gc_ms > a.busy_ms * .15 && a.gc_ms / runs_of(meta) > 5) warn("gc-heavy", `Garbage collection took ${fmt_pct(a.gc_ms, a.busy_ms)} of busy time (${fmt_ms(pr(a.gc_ms))} ms${per_r}) — see the allocators for who creates the garbage.`);
|
|
904
|
+
if (extras.retained && extras.retained.total_bytes >= 5242880) {
|
|
905
|
+
const r = extras.retained;
|
|
906
|
+
const top = r.sites.slice(0, 3).map((s) => `${s.name}${s.caller ? ` via ${s.caller}` : ""}${s.component ? ` in ${s.component}` : ""} (${Math.round(s.bytes / 1048576 * 10) / 10} MB)`).join(", ");
|
|
907
|
+
const mb = r.total_bytes / 1048576;
|
|
908
|
+
const limit = meta.lambda ? meta.lambda_mb : meta.heap_limit_mb;
|
|
909
|
+
const runway = limit ? Math.max(1, Math.floor(limit / mb)) : null;
|
|
910
|
+
const pkg_of = (file) => {
|
|
911
|
+
const at = file.lastIndexOf("/node_modules/");
|
|
912
|
+
if (at === -1) return null;
|
|
913
|
+
const parts = file.slice(at + 14).split("/");
|
|
914
|
+
return parts[0].startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
915
|
+
};
|
|
916
|
+
const from_pkg = r.sites.filter((s) => s.url && pkg_of(s.url));
|
|
917
|
+
const pkg_share = from_pkg.reduce((sum, s) => sum + s.bytes, 0) / r.total_bytes;
|
|
918
|
+
const pkg = pkg_share >= .5 ? pkg_of(from_pkg[0].url) : null;
|
|
919
|
+
warn("retained-per-render", `One render leaves ${Math.round(mb * 10) / 10} MB alive after a full collection: ${top}. Every render adds that much; the instance grows until it restarts` + (runway ? ` — at this rate its ${limit} MB ${meta.lambda ? "of memory" : "heap"} runs out after about ${runway} render${runway === 1 ? "" : "s"} of this page, and the process dies.` : ".") + (pkg ? ` ${Math.round(pkg_share * 100)}% of it is held inside ${pkg}.` : ""), {
|
|
920
|
+
fix: pkg ? `The kept memory is ${pkg}'s: called once per render (or per element), it keeps each call's state. Call it once and reuse it — one instance or one document-wide pass instead of one call per element — and check the package's issues for a dispose or cleanup call; restarting workers every N requests is only a stopgap.` : "Whatever holds these (a module-level cache, a registry, a closure kept by a long-lived object) must release them or be bounded. The sites named are where the kept objects were made; what keeps them is their owner.",
|
|
921
|
+
...r.sites[0]?.url ? {
|
|
922
|
+
file: r.sites[0].url,
|
|
923
|
+
line: r.sites[0].line
|
|
924
|
+
} : {}
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
for (const d of a.deopts.slice(0, 3)) {
|
|
928
|
+
if (d.self_ms < Math.max(2, a.busy_ms * .01) || d.count < 2) continue;
|
|
929
|
+
const why = Object.entries(d.reasons).sort((x, y) => y[1] - x[1]).map(([r, n]) => `${r} ×${n}`).join(", ");
|
|
930
|
+
warn("deopt", `${d.name} was deoptimized ${d.count} times in the window (${why}) and cost ${fmt_ms(pr(d.self_ms))} ms of CPU${per_r}: it runs in slow code most of the time.`, {
|
|
931
|
+
fix: "A deopt reason names the assumption that broke: \"wrong map\" is objects of different shapes at one site (keep the same properties in the same order), \"not a Smi\" is a number that became a float or a string, a megamorphic call site is many types through one call. Give the function one shape and it stays optimized.",
|
|
932
|
+
anchor: `fn:${d.key}`,
|
|
933
|
+
file: d.url,
|
|
934
|
+
line: d.line
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
const sync = sync_io(a).filter((r) => !meta.dev || r.callers.length);
|
|
938
|
+
const sync_ms = sync.reduce((s, r) => s + r.total_ms, 0);
|
|
939
|
+
if (sync_ms >= 2) {
|
|
940
|
+
const top = sync[0];
|
|
941
|
+
warn("sync-io", `${fmt_ms(pr(sync_ms))} ms of synchronous I/O on the CPU${R > 1 ? " per render" : " during the window"}: ${sync.slice(0, 3).map((s) => `${s.name} ${fmt_ms(pr(s.total_ms))} ms${s.callers[0] ? ` from ${s.callers[0]}` : ""}`).join(", ")}. While it runs no other request on this instance moves.`, {
|
|
942
|
+
fix: "Use the async form (fs.promises, zlib promises, execFile with a callback), or do it once at start-up and keep the result.",
|
|
943
|
+
anchor: `fn:${top.key}`
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
if (extras.promises) {
|
|
947
|
+
const all = meta.runs?.length ? extras.promises.count / meta.runs.length : extras.promises.count;
|
|
948
|
+
const dev_share = meta.dev ? extras.promises.top.reduce((s, t) => s + (dev_tool_caller(t.caller) ? t.share : 0), 0) : 0;
|
|
949
|
+
const per = all * (1 - Math.min(dev_share, 1));
|
|
950
|
+
if (per >= 1e4) {
|
|
951
|
+
const rest = 1 - dev_share || 1;
|
|
952
|
+
const top = extras.promises.top.filter((t) => !meta.dev || !dev_tool_caller(t.caller)).slice(0, 3).map((t) => `${t.caller} ${Math.round(t.share / rest * 100)}%`).join(", ");
|
|
953
|
+
const left_out = dev_share >= .05 ? ` (the dev server's own module loading made about ${Math.round(all - per).toLocaleString()} more, left out)` : "";
|
|
954
|
+
const renderer = extras.promises.top.filter((t) => t.caller.startsWith("#collect_content_async") && t.caller.endsWith("(svelte)")).reduce((s, t) => s + t.share, 0) / (1 - dev_share || 1);
|
|
955
|
+
if (renderer >= .5) {
|
|
956
|
+
const many = a.components.filter((c) => (c.calls ?? 0) >= 50).sort((x, y) => (y.calls ?? 0) - (x.calls ?? 0)).slice(0, 3).map((c) => `${c.name} ×${(c.calls ?? 0).toLocaleString()}`);
|
|
957
|
+
warn("promise-storm", `${Math.round(per).toLocaleString()} promises per render${left_out}, ${Math.round(renderer * 100)}% of them from Svelte's async server renderer (#collect_content_async): it makes one for each component and block it renders, so the count follows how much the page renders, not an await of yours.${many.length ? ` The components rendered most: ${many.join(", ")}.` : ""} Each is an allocation and a microtask, a cost no single function shows.`, { fix: `Render fewer pieces${many.length ? ` — ${many[0].split(" ×")[0]} first` : ""}: inline a small component an \`{#each}\` repeats into the loop's own markup, stop a component that renders itself sooner, or send a page of the list. The renderer goes async for every page when Svelte's \`experimental.async\` is on: an app with no \`await\` in its markup can turn it off and render with none of them.` });
|
|
958
|
+
} else warn("promise-storm", `${Math.round(per).toLocaleString()} promises per render${left_out}. Each is an allocation and a microtask; at this volume they are a cost no single function shows.${top ? ` Mostly from: ${top}.` : left_out ? " Their makers are spread thin: no one origin of the app’s stands out in the sample — profile again warm (the dev server’s loading crowds a first render)." : ""}`, { fix: "Find the loop that awaits per item (a render per tag, a fetch per row) and do the work in one call, or on a plain array without async at all." });
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
{
|
|
962
|
+
const by = /* @__PURE__ */ new Map();
|
|
963
|
+
for (const o of extras.io ?? []) {
|
|
964
|
+
if (!o.left_each_run || !o.caller) continue;
|
|
965
|
+
const g = by.get(o.caller) ?? {
|
|
966
|
+
caller: o.caller,
|
|
967
|
+
n: 0,
|
|
968
|
+
...o.repeat !== void 0 ? { repeat: o.repeat } : { delay: o.delay },
|
|
969
|
+
...o.caller_at ? { at: o.caller_at } : {}
|
|
970
|
+
};
|
|
971
|
+
g.n++;
|
|
972
|
+
by.set(o.caller, g);
|
|
973
|
+
}
|
|
974
|
+
for (const g of [...by.values()].slice(0, 2)) {
|
|
975
|
+
const s = (ms) => ms >= 1e3 ? `${Math.round(ms / 100) / 10} s` : `${ms} ms`;
|
|
976
|
+
warn("render-leftover", `Each render ${g.repeat !== void 0 ? `starts an interval (every ${s(g.repeat)}) that is still running after the render` : `schedules a ${s(g.delay ?? 0)} timer that is still waiting after the response went out`}: ${g.caller}. Every render adds ${g.n === 1 ? "one" : g.n} more — under traffic they pile up, each keeping its request's data in memory${g.repeat !== void 0 ? " and running on the instance’s CPU" : ""}.`, {
|
|
977
|
+
fix: g.repeat !== void 0 ? "Start it once for the process (at module level, or behind a flag set on first use) and share what it keeps fresh, or clear it when the work it serves is done. A render should leave nothing running." : "Clear the timer when the work it guards finishes, or keep one process-wide timer (a cache sweep) instead of one per request.",
|
|
978
|
+
...g.at ? {
|
|
979
|
+
file: g.at.path,
|
|
980
|
+
line: g.at.line
|
|
981
|
+
} : {}
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
for (const m of memo_candidates(a, extras.gc_attr?.makers ?? [], 8, runs_of(meta)).slice(0, 2)) info("memo-candidate", `${m.name} runs ${m.calls} times per render at ${m.per_call_ms} ms each (${fmt_ms(m.total_ms)} ms)${m.alloc_per_call ? `, allocating ${Math.round(m.alloc_per_call / 1024)} KB per call` : ""}${m.parent ? `, mostly under ${m.parent}` : ""}. If its result depends only on its argument, a cache keyed on it runs it once per distinct value.`, {
|
|
986
|
+
anchor: `fn:${m.key}`,
|
|
987
|
+
file: m.url,
|
|
988
|
+
line: m.line
|
|
989
|
+
});
|
|
990
|
+
const g = extras.gc_attr;
|
|
991
|
+
if (g && g.summary.total_ms >= 5 && g.makers.length) {
|
|
992
|
+
const m = g.makers[0];
|
|
993
|
+
if (m.gc_ms >= 3 && m.share >= .15) {
|
|
994
|
+
const where = m.component && m.component !== m.name ? ` inside ${m.component}` : "";
|
|
995
|
+
const via = m.caller && m.caller !== m.name ? ` (called from ${m.caller})` : "";
|
|
996
|
+
warn("gc-cause", `${fmt_ms(pr(m.gc_ms))} ms of the ${fmt_ms(pr(g.summary.total_ms))} ms of GC${per_r} is the garbage ${m.name}${via} makes${where}: ${Math.round(pr(m.allocated) / 1048576 * 10) / 10} MB of the ${Math.round(pr(g.summary.allocated_mb) * 10) / 10} MB allocated${R > 1 ? " per render" : " in the window"} (${Math.round(m.share * 100)}%)${m.pauses ? `, on the causing side of ${m.pauses} pause${m.pauses === 1 ? "" : "s"}` : `, across the window's ${g.summary.count} pause${g.summary.count === 1 ? "" : "s"}`}.`, {
|
|
997
|
+
fix: `Allocate less there: reuse the object between calls, avoid a clone or a JSON round trip of a large value, build strings once instead of in a loop. ${g.summary.retained_mb !== void 0 && g.summary.retained_mb > 20 ? `The heap also grew ${g.summary.retained_mb} MB over the window: something keeps what it allocates.` : "Most of it is churn: the heap did not grow with it."}`,
|
|
998
|
+
...m.url ? {
|
|
999
|
+
file: m.url,
|
|
1000
|
+
line: m.line
|
|
1001
|
+
} : {}
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const al = extras.alloc;
|
|
1006
|
+
if (al && al.bursts.length && al.grown_mb >= 20) {
|
|
1007
|
+
const b = al.bursts[0];
|
|
1008
|
+
if (b.mb >= al.grown_mb * .3) {
|
|
1009
|
+
const who = b.running[0];
|
|
1010
|
+
info("alloc-burst", `${b.mb} MB of the ${al.grown_mb} MB the heap grew came in one ${fmt_ms(b.t1 - b.t0)} ms stretch (${b.rate} MB/s)${who ? `, while ${who.label} ran` : ""}${b.gc ? " — and a collection fell inside it, so the allocation was more than the growth shows" : ""}.`, {
|
|
1011
|
+
fix: "Look at the allocators table for that stretch: the makers with a caller there are the ones filling the heap that fast.",
|
|
1012
|
+
...who?.file ? { file: who.file } : {}
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (meta.runs_set_aside) {
|
|
1017
|
+
const kept = meta.runs?.length ?? 0;
|
|
1018
|
+
const total = kept + meta.runs_set_aside;
|
|
1019
|
+
(kept < 2 ? warn : info)("runs-set-aside", `${meta.runs_set_aside} of the ${total} renders ran while another visitor asked for this same page, and were left out: their work runs the same lines, so nothing but the clock tells it apart. The report reads the ${kept} render${kept === 1 ? "" : "s"} that ran clean.`, kept < 2 ? { fix: "Record again when the page is quieter (or on an instance out of rotation) for more than one clean render." } : {});
|
|
1020
|
+
}
|
|
1021
|
+
const ct = extras.contention;
|
|
1022
|
+
if (ct && ct.requests.length) {
|
|
1023
|
+
const others = ct.requests.filter((r) => r.kind === "other");
|
|
1024
|
+
const others_n = ct.counts?.other ?? others.length;
|
|
1025
|
+
const holes = ct.counts?.hole ?? ct.requests.filter((r) => r.kind === "hole").length;
|
|
1026
|
+
const win = meta.trigger === "page" ? Math.max(meta.runs?.length ?? 1, 1) : 1;
|
|
1027
|
+
if (others_n && ct.busy_share >= .1) {
|
|
1028
|
+
const top = others.slice(0, 3).map((r) => `${r.method} ${r.path}`).join(", ");
|
|
1029
|
+
const page_path = meta.page?.split("?")[0];
|
|
1030
|
+
const same = page_path ? others.filter((r) => r.path === page_path).length : 0;
|
|
1031
|
+
warn("busy-instance", `${others_n} other request${others_n === 1 ? "" : "s"} ran on this instance during the profiled render${win > 1 ? "s" : ""} (${top}${others_n > 3 ? ", …" : ""}), in flight for ${Math.round(ct.busy_share * 100)}% of the window: up to ${fmt_ms(ct.cpu_max_ms)} ms of its wall time was the event loop serving them, and the render's own numbers carry that wait.${a.other_requests_ms ? ` The samples under other pages' code (${fmt_ms(a.other_requests_ms / win)} ms a render) were set aside, and so were the slow lines in their page files — but a shared helper they called after an await has no frame of theirs left to tell it by, and may still show here: profile again on a quiet instance to be sure.` : ""}${same ? meta.runs_set_aside ? ` ${same} of them asked for this same page; the ${meta.runs_set_aside} render${meta.runs_set_aside === 1 ? "" : "s"} they overlapped ${meta.runs_set_aside === 1 ? "was" : "were"} left out, so this report reads the ${meta.runs?.length ?? 0} that ran clean.` : ` ${same} of them asked for this same page: their work runs through the same lines and cannot be told apart from the profiled render's (every render overlapped one), so this report's CPU reads high — up to ${same + 1}× on the lines they share.` : ""}`, { fix: "Record again on a quiet instance, or read the CPU numbers (they exclude the others) rather than the wall time. Sustained, this is what horizontal scaling or a worker pool is for." });
|
|
1032
|
+
}
|
|
1033
|
+
const selfs = ct.requests.filter((r) => r.kind === "self");
|
|
1034
|
+
const self_n = ct.counts?.self ?? selfs.length;
|
|
1035
|
+
if (self_n) {
|
|
1036
|
+
const paths = ct.counts?.self_paths ?? [...new Set(selfs.map((r) => r.path))];
|
|
1037
|
+
const per = Math.round(self_n / win);
|
|
1038
|
+
const self_ms = (ct.counts?.self_ms ?? selfs.reduce((s, r) => s + r.ms, 0)) / win;
|
|
1039
|
+
const remote_n = ct.counts?.self_remote ?? selfs.filter((r) => r.remote).length;
|
|
1040
|
+
if (remote_n && remote_n === self_n) {
|
|
1041
|
+
const names = [...new Set(paths.map(remote_function_name))];
|
|
1042
|
+
info("self-fetch", `The render read ${names.length === 1 ? `a \`prerender\` remote function's answer (${names[0]})` : `\`prerender\` remote functions' answers (${names.slice(0, 3).join(", ")}${names.length > 3 ? ", …" : ""})`} over HTTP ${per} time${per === 1 ? "" : "s"}: Kit fetches it from the site's own public address on every server render — here this same instance answered, ${fmt_ms(self_ms)} ms of requests while it was rendering. Where the host serves the prerendered answer as a file it is still a trip out and back per render; where it does not (an argument the build did not prerender), the function runs anyway, in a request of its own.`, { fix: "For data a render needs, use a `query` (it runs in the render's own process, no request) or read it in the load; keep `prerender` for answers the browser fetches." });
|
|
1043
|
+
} else info("self-fetch", `The render called its own server ${per} time${per === 1 ? "" : "s"} (${paths.slice(0, 3).join(", ")}${paths.length > 3 ? ", …" : ""}), ${fmt_ms(self_ms)} ms of requests answered by the same event loop that was rendering: the page waited on itself, and every one of those calls paid a full HTTP round trip to reach code in the same process.`, { fix: "Call the function behind the endpoint directly from the load (import it), or use a remote function; keep fetch for servers that are not this one." });
|
|
1044
|
+
}
|
|
1045
|
+
if (holes && !others_n && !self_n) info("holes-in-flight", `${holes} of the page's own hole request${holes === 1 ? "" : "s"} ${holes === 1 ? "was" : "were"} answered while it rendered: the deferred islands cost the instance CPU on the page's own clock.`);
|
|
1046
|
+
}
|
|
1047
|
+
const ln = extras.lineage;
|
|
1048
|
+
if (ln) {
|
|
1049
|
+
const route = own_requests(meta).find((r) => r.route)?.route ?? meta.request?.route;
|
|
1050
|
+
const unread = ln.unread.filter((k) => k.from && !(route && another_routes_file(route, k.from)));
|
|
1051
|
+
if (unread.length) {
|
|
1052
|
+
const names = unread.slice(0, 4).map((k) => `${k.key} (${k.from})`).join(", ");
|
|
1053
|
+
const load_wait = /* @__PURE__ */ new Map();
|
|
1054
|
+
for (const k of unread) if ((k.load_wait_ms ?? 0) > 0 && k.from) load_wait.set(k.from, k.load_wait_ms);
|
|
1055
|
+
const wait = [...load_wait.values()].reduce((s, w) => s + w, 0);
|
|
1056
|
+
const loads = load_wait.size;
|
|
1057
|
+
const rows = unread_rows(extras.drill);
|
|
1058
|
+
const cut = rows.reduce((s, r) => s + r.ms, 0);
|
|
1059
|
+
const exact = rows.length ? ` Exactly what builds only ${unread.length === 1 ? "it" : "them"}: ${rows.slice(0, 4).map((r) => `${r.label} ${fmt_ms(r.ms)} ms (${r.kind === "wait" ? "waiting" : "CPU"}) → ${r.keys.join(", ")}`).join(", ")}${rows.length > 4 ? ", …" : ""} — ${fmt_ms(cut)} ms of one render for nothing.` : wait > 0 ? ` The load${loads > 1 ? "s" : ""} returning ${unread.length === 1 ? "it" : "them"} waited ${fmt_ms(wait)} ms on upstream calls per render in all; the calls that feed only ${unread.length === 1 ? "this key" : "these keys"} are the part to cut.` : "";
|
|
1060
|
+
warn("key-unread", `${unread.length} page.data key${unread.length === 1 ? "" : "s"} no component reads: ${names}${unread.length > 4 ? ", …" : ""}.${exact}`, {
|
|
1061
|
+
fix: "Drop the key from the load, or the call that produces it — nothing on the page uses it. A key read through a spread or a whole-object pass-through would show as unknown, not unread.",
|
|
1062
|
+
file: unread[0].from
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
const so = ln.server_only.filter((k) => k.shipped_bytes >= 2048);
|
|
1066
|
+
if (so.length) {
|
|
1067
|
+
const bytes = so.reduce((s, k) => s + k.shipped_bytes, 0);
|
|
1068
|
+
info("seed-server-only", `${fmt_kb(bytes)} of the seed is ${so.length} key${so.length === 1 ? "" : "s"} only the server renders (${so.slice(0, 4).map((k) => k.key).join(", ")}${so.length > 4 ? ", …" : ""}): shipped to the browser, read by no island.`, { fix: "The seed ships the keys islands read; a key here is read by a server component through a name the shaping could not see. Check the island’s closure." });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
for (const v of span_values(extras.spans)) if (v.r !== void 0 && v.r >= .8 && v.n >= 5 && v.ms_per_unit !== void 0 && v.ms_per_unit > 0) {
|
|
1072
|
+
info("value-driven", `${v.span} scales with ${v.attr}: about ${v.ms_per_unit >= .01 ? v.ms_per_unit : v.ms_per_unit.toExponential(1)} ms per unit over ${v.n} spans (${v.attr} ${v.min}–${v.max}, fit r=${v.r}). Halving the ${v.attr} halves the span.`);
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
if (extras.gc && extras.gc.max_ms > 20) warn("gc-pause", `Longest single GC pause: ${fmt_ms(extras.gc.max_ms)} ms (${extras.gc.count} pauses in the window, ${fmt_ms(pr(extras.gc.total_ms))} ms${R > 1 ? " per render" : " total"}). A long pause freezes every request at once.`);
|
|
1076
|
+
if (meta.loop_delay && meta.loop_delay.p99 > 50) warn("loop-stall", `The event loop stalled up to ${fmt_ms(meta.loop_delay.p99)} ms (p99) — long synchronous work blocks every other request.`);
|
|
1077
|
+
const mem_delta = extras.mem.length >= 2 ? extras.mem.at(-1).rss - extras.mem[0].rss : 0;
|
|
1078
|
+
const kept_said = !!extras.retained && extras.retained.total_bytes >= 5242880;
|
|
1079
|
+
if (mem_delta > 50 && !kept_said) warn("mem-growth", `The server's memory grew ${mem_delta} MB while the recording ran. A first recording grows it as caches and the heap reach their working size; one that grows it again each time is keeping something per render.`, { fix: "Record again: if it grows by about as much again, look for what a render adds to a module-level map, array or cache that is never cleared — the memory card lists what one render leaves alive." });
|
|
1080
|
+
const profiler_ms = a.buckets.find((b) => b.key === "profiler overhead")?.self_ms ?? 0;
|
|
1081
|
+
if (profiler_ms > a.busy_ms * .05 && profiler_ms > 5) info("profiler-overhead", `${fmt_ms(profiler_ms)} ms of busy time is the profiler itself — a one-time cost when recording starts. Your app did not pay this outside the recording.`);
|
|
1082
|
+
if (meta.dev) info("dev-mode", "Recorded on the dev server — Vite module loading and transforms are included. Build and run production for exact figures.");
|
|
1083
|
+
if (meta.run_static) {
|
|
1084
|
+
for (let k = out.length - 1; k >= 0; k--) if (RENDER_ONLY.has(out[k].code)) out.splice(k, 1);
|
|
1085
|
+
}
|
|
1086
|
+
const gap = out.find((f) => f.code === "lcp-gap");
|
|
1087
|
+
if (gap && out.some((f) => f.code === "slow-lcp")) {
|
|
1088
|
+
const dash = gap.message.indexOf(" — ");
|
|
1089
|
+
if (dash !== -1) gap.message = `${gap.message.slice(0, dash)}: the server is not the bottleneck. Where the browser's wait went is the largest paint's own card (In the browser: LCP …).`;
|
|
1090
|
+
gap.fix = "The wait is the browser's, not the server's: follow the largest paint's own card (In the browser: LCP …), which names the part that cost most and its fix.";
|
|
1091
|
+
}
|
|
1092
|
+
if (extras.hazards) {
|
|
1093
|
+
const told = /* @__PURE__ */ new Set();
|
|
1094
|
+
const rows = island_rows_of(meta);
|
|
1095
|
+
for (const f of out) if (f.code === "markup-changed" || f.code === "hydration-mismatch") {
|
|
1096
|
+
for (const fp of f.fps ?? []) for (const r of rows) if (r.fp === fp) told.add(r.entry);
|
|
1097
|
+
}
|
|
1098
|
+
if (out.some((f) => f.code === "hydration-mismatch")) {
|
|
1099
|
+
for (const c of extras.client ?? []) if (c.recovered > 0) told.add(c.entry);
|
|
1100
|
+
}
|
|
1101
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1102
|
+
const reads = [];
|
|
1103
|
+
for (const r of rows) {
|
|
1104
|
+
if (r.wake === "none" || seen.has(r.entry) || told.has(r.entry)) continue;
|
|
1105
|
+
seen.add(r.entry);
|
|
1106
|
+
const line = (extras.hazards[r.entry] ?? []).find((h) => h.kind === "browser" && !h.guard);
|
|
1107
|
+
if (line) reads.push({
|
|
1108
|
+
name: island_name(r),
|
|
1109
|
+
line
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
const who = reads.map((x) => x.name);
|
|
1113
|
+
if (reads.length) info("island-browser-read", `${who.length <= 3 ? who.join(", ") : `${who.slice(0, 3).join(", ")} and ${who.length - 3} more`} ${reads.length === 1 ? "reads" : "read"} a browser-only value while rendering, so the browser draws ${reads.length === 1 ? "it" : "them"} differently and the markup changes as ${reads.length === 1 ? "it wakes" : "they wake"}: ${hazard_words(reads.map((x) => x.line)).slice(32)}`, {
|
|
1114
|
+
fix: hazard_fix(reads.map((x) => x.line)),
|
|
1115
|
+
file: reads[0].line.file,
|
|
1116
|
+
line: reads[0].line.line
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
if (extras.hazards) {
|
|
1120
|
+
const mc = out.find((f) => f.code === "markup-changed");
|
|
1121
|
+
if (mc?.fps?.length) {
|
|
1122
|
+
const rows = island_rows_of(meta);
|
|
1123
|
+
const lines = [...new Set(mc.fps.flatMap((fp) => rows.filter((r) => r.fp === fp).map((r) => r.entry)))].flatMap((e) => extras.hazards?.[e] ?? []);
|
|
1124
|
+
if (lines.length) {
|
|
1125
|
+
mc.message += ` ${hazard_words(lines)}`;
|
|
1126
|
+
mc.fix = `${hazard_fix(lines)} ${mc.fix ?? ""}`.trim();
|
|
1127
|
+
mc.file ??= lines[0].file;
|
|
1128
|
+
mc.line ??= lines[0].line;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (extras.patterns?.length) link_patterns(out, extras.patterns, a);
|
|
1133
|
+
return out;
|
|
1134
|
+
}
|
|
1135
|
+
/** `lib/hell/ds-ssr.ts` and `src/lib/hell/ds-ssr.ts` name one file: one path ends with the other */
|
|
1136
|
+
const same_file = (x, y) => {
|
|
1137
|
+
const a = x.startsWith("file://") ? x.slice(7) : x;
|
|
1138
|
+
const b = y.startsWith("file://") ? y.slice(7) : y;
|
|
1139
|
+
return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
|
|
1140
|
+
};
|
|
1141
|
+
/** an identifier no sentence would contain: an inner capital, an underscore or a digit */
|
|
1142
|
+
const looks_like_code = (name) => {
|
|
1143
|
+
for (let i = 1; i < name.length; i++) {
|
|
1144
|
+
const c = name.charCodeAt(i);
|
|
1145
|
+
if (c >= 65 && c <= 90 || c === 95 || c >= 48 && c <= 57) return true;
|
|
1146
|
+
}
|
|
1147
|
+
return false;
|
|
1148
|
+
};
|
|
1149
|
+
/** every `(path:line)` a finding's message names (a caller written `fn (lib/x.ts:46)`) */
|
|
1150
|
+
function places_in(message) {
|
|
1151
|
+
const out = [];
|
|
1152
|
+
let i = message.indexOf("(");
|
|
1153
|
+
while (i !== -1) {
|
|
1154
|
+
const close = message.indexOf(")", i);
|
|
1155
|
+
if (close === -1) break;
|
|
1156
|
+
const inner = message.slice(i + 1, close);
|
|
1157
|
+
const colon = inner.lastIndexOf(":");
|
|
1158
|
+
const line = colon > 0 ? Number(inner.slice(colon + 1)) : NaN;
|
|
1159
|
+
if (Number.isInteger(line) && line > 0 && !inner.slice(0, colon).includes(" ")) out.push({
|
|
1160
|
+
file: inner.slice(0, colon),
|
|
1161
|
+
line
|
|
1162
|
+
});
|
|
1163
|
+
i = message.indexOf("(", i + 1);
|
|
1164
|
+
}
|
|
1165
|
+
return out;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* A finding that a slow pattern already explains points at it (`pattern`, its index), so the two
|
|
1169
|
+
* read as one problem: a finding and a pattern share a code line (a site, a caller line, or the
|
|
1170
|
+
* function a site sits in), the row the finding opens is a site's function, or a network finding
|
|
1171
|
+
* names the calls a waits-in-a-row site made. Nothing is hidden; the finding gains the link.
|
|
1172
|
+
*/
|
|
1173
|
+
function link_patterns(findings, patterns, a) {
|
|
1174
|
+
const start_of = /* @__PURE__ */ new Map();
|
|
1175
|
+
for (const f of a.functions) start_of.set(f.key, f.line);
|
|
1176
|
+
for (const c of a.components) start_of.set(c.key, c.line);
|
|
1177
|
+
const places = patterns.map((p) => {
|
|
1178
|
+
const lines = [];
|
|
1179
|
+
const fns = /* @__PURE__ */ new Set();
|
|
1180
|
+
const targets = [];
|
|
1181
|
+
/** a site's function as a finding names a caller: `drainQueue (` */
|
|
1182
|
+
const callers = [];
|
|
1183
|
+
for (const s of p.sites) {
|
|
1184
|
+
lines.push({
|
|
1185
|
+
file: s.path || s.file,
|
|
1186
|
+
line: s.line
|
|
1187
|
+
});
|
|
1188
|
+
if (s.fn_name && !s.fn_name.startsWith("(") && looks_like_code(s.fn_name)) callers.push(s.fn_name + " (");
|
|
1189
|
+
if (s.fn) {
|
|
1190
|
+
fns.add(s.fn);
|
|
1191
|
+
const at = start_of.get(s.fn);
|
|
1192
|
+
if (at) lines.push({
|
|
1193
|
+
file: s.path || s.file,
|
|
1194
|
+
line: at
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
for (const v of s.via ?? []) lines.push({
|
|
1198
|
+
file: v.path || v.file,
|
|
1199
|
+
line: v.line
|
|
1200
|
+
});
|
|
1201
|
+
if (s.target) targets.push(s.target.split(" and ")[0]);
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
lines,
|
|
1205
|
+
fns,
|
|
1206
|
+
targets,
|
|
1207
|
+
callers
|
|
1208
|
+
};
|
|
1209
|
+
});
|
|
1210
|
+
const by_code = {
|
|
1211
|
+
"seed-whole": "seed-whole-read",
|
|
1212
|
+
"seed-large": "seed-whole-read",
|
|
1213
|
+
"retained-per-render": "kept-per-render",
|
|
1214
|
+
"slower-each-run": "kept-per-render"
|
|
1215
|
+
};
|
|
1216
|
+
for (const f of findings) {
|
|
1217
|
+
const kind = by_code[f.code];
|
|
1218
|
+
const direct = kind ? patterns.findIndex((p) => p.kind === kind) : -1;
|
|
1219
|
+
if (direct !== -1) {
|
|
1220
|
+
f.pattern = direct;
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (f.severity !== "warn" && f.code !== "memo-candidate") continue;
|
|
1224
|
+
const mine = [...f.file && f.line ? [{
|
|
1225
|
+
file: f.file,
|
|
1226
|
+
line: f.line
|
|
1227
|
+
}] : [], ...places_in(f.message)];
|
|
1228
|
+
const anchor_fn = f.anchor?.startsWith("fn:") ? f.anchor.slice(3) : f.anchor?.startsWith("comp:") ? "C:" + f.anchor.slice(5) : void 0;
|
|
1229
|
+
const hit = places.findIndex((p) => anchor_fn !== void 0 && p.fns.has(anchor_fn) || mine.some((m) => p.lines.some((l) => l.line === m.line && same_file(l.file, m.file))) || p.callers.some((c) => f.message.includes(c)) || p.targets.some((t) => {
|
|
1230
|
+
const bare = t.slice(t.indexOf(" ") + 1);
|
|
1231
|
+
return f.message.includes(bare);
|
|
1232
|
+
}));
|
|
1233
|
+
if (hit !== -1) f.pattern = hit;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
/** The island rows of the profiled page: the page request that recorded them (detail is on only
|
|
1237
|
+
* while the profiler records, so the internal render carries them). */
|
|
1238
|
+
function island_rows_of(meta) {
|
|
1239
|
+
return own_requests(meta).find((r) => r.og?.island_rows?.length)?.og?.island_rows ?? [];
|
|
1240
|
+
}
|
|
1241
|
+
const HOST_FN_RE = /^_[0-9a-f]{11}$/;
|
|
1242
|
+
const ISLAND_ID_RE = /([0-9a-f]{12})/;
|
|
1243
|
+
/**
|
|
1244
|
+
* THE ISLAND HOST WRAPPERS by name. The compiler wraps every island in a virtual
|
|
1245
|
+
* `wrapper/<id>.svelte`, and Svelte names that function after the file: `_b95bfb97fab` (the id
|
|
1246
|
+
* minus its first character, made an identifier). The island rows know the id (in the entry URL)
|
|
1247
|
+
* and the component: this returns the `rename` hook `analyze()` takes, so the wrapper reads
|
|
1248
|
+
* `ProductCard (island host)` in every table, stack and flame. Rows come from every request in
|
|
1249
|
+
* the window (a page profile's own render, a trapped page, a header-profiled request).
|
|
1250
|
+
*/
|
|
1251
|
+
function island_host_renamer(requests) {
|
|
1252
|
+
const by_suffix = /* @__PURE__ */ new Map();
|
|
1253
|
+
for (const r of requests) for (const row of r.og?.island_rows ?? []) {
|
|
1254
|
+
const id = ISLAND_ID_RE.exec(row.entry)?.[1];
|
|
1255
|
+
if (!id) continue;
|
|
1256
|
+
const name = island_name(row);
|
|
1257
|
+
if (name && name !== row.entry) by_suffix.set(id.slice(1), name);
|
|
1258
|
+
}
|
|
1259
|
+
if (!by_suffix.size) return void 0;
|
|
1260
|
+
return (name) => {
|
|
1261
|
+
if (!HOST_FN_RE.test(name)) return void 0;
|
|
1262
|
+
const island = by_suffix.get(name.slice(1));
|
|
1263
|
+
return island ? `${island} (island host)` : void 0;
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
/** What to call a hole: its component and props (`Recommendations {"forProduct":"P1"}`), else its id. */
|
|
1267
|
+
function hole_label(h) {
|
|
1268
|
+
if (!h.name) return h.id;
|
|
1269
|
+
return h.props ? `${h.name} ${h.props}` : h.name;
|
|
1270
|
+
}
|
|
1271
|
+
/** The hole endpoint requests in the window, per hole id: hits / misses / uncached renders. */
|
|
1272
|
+
function hole_economics(meta) {
|
|
1273
|
+
const out = /* @__PURE__ */ new Map();
|
|
1274
|
+
for (const r of meta.requests) {
|
|
1275
|
+
if (!r.hole) continue;
|
|
1276
|
+
const e = out.get(r.hole.id) ?? {
|
|
1277
|
+
id: r.hole.id,
|
|
1278
|
+
hit: 0,
|
|
1279
|
+
miss: 0,
|
|
1280
|
+
none: 0,
|
|
1281
|
+
ms: 0,
|
|
1282
|
+
ttl: r.hole.ttl
|
|
1283
|
+
};
|
|
1284
|
+
e[r.hole.cache]++;
|
|
1285
|
+
e.ms += r.ms;
|
|
1286
|
+
out.set(r.hole.id, e);
|
|
1287
|
+
}
|
|
1288
|
+
return out;
|
|
1289
|
+
}
|
|
1290
|
+
/** The display name of an island: its component's name when the row carries it, else the
|
|
1291
|
+
* entry's basename (`ProductCard` from `.../ProductCard.svelte`; a built facade keeps its file). */
|
|
1292
|
+
function island_name(row) {
|
|
1293
|
+
if (typeof row !== "string" && row.name) return row.name;
|
|
1294
|
+
const entry = typeof row === "string" ? row : row.entry;
|
|
1295
|
+
const m = /([^/\\?]+)\.svelte(?:[?#].*)?$/.exec(entry);
|
|
1296
|
+
return m ? m[1] : entry.replace(/^.*[/\\]/, "") || entry;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* ONE ROW PER ISLAND: the tail records a row per fingerprint (a list of 48 cards is 48 props
|
|
1300
|
+
* sidecars, one each), and the fingerprint is what the browser's beacon joins on — but the table
|
|
1301
|
+
* and the findings speak of the island. Rows with the same entry and wake merge: copies and
|
|
1302
|
+
* bytes add up, references and hints union, the first devalue culprit stands for all, and
|
|
1303
|
+
* `variants` says how many fingerprints went in.
|
|
1304
|
+
*/
|
|
1305
|
+
function group_islands(rows) {
|
|
1306
|
+
const out = /* @__PURE__ */ new Map();
|
|
1307
|
+
for (const r of rows) {
|
|
1308
|
+
const key = r.entry + "\0" + r.wake;
|
|
1309
|
+
const g = out.get(key);
|
|
1310
|
+
if (!g) {
|
|
1311
|
+
out.set(key, {
|
|
1312
|
+
...r,
|
|
1313
|
+
ref_keys: [...r.ref_keys],
|
|
1314
|
+
hints: [...r.hints],
|
|
1315
|
+
variants: 1
|
|
1316
|
+
});
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
g.count += r.count;
|
|
1320
|
+
g.variants = (g.variants ?? 1) + 1;
|
|
1321
|
+
g.props_bytes += r.props_bytes;
|
|
1322
|
+
g.canonical_bytes += r.canonical_bytes;
|
|
1323
|
+
g.refs += r.refs;
|
|
1324
|
+
g.json = g.json && r.json;
|
|
1325
|
+
g.culprit ??= r.culprit;
|
|
1326
|
+
g.name ||= r.name;
|
|
1327
|
+
g.interactivity ??= r.interactivity;
|
|
1328
|
+
for (const k of r.ref_keys) if (!g.ref_keys.includes(k)) g.ref_keys.push(k);
|
|
1329
|
+
for (const h of r.hints) if (!g.hints.includes(h)) g.hints.push(h);
|
|
1330
|
+
}
|
|
1331
|
+
for (const g of out.values()) g.ref_keys.sort();
|
|
1332
|
+
return [...out.values()];
|
|
1333
|
+
}
|
|
1334
|
+
/** Unique bytes of an island's JS closure (its module + preload hints), when the weights are known. */
|
|
1335
|
+
/** Code every page with islands loads once, never an island's own to change: the runtimes, Kit,
|
|
1336
|
+
* and devalue (the runtime's decoder for rich props — the `props-devalue` finding is where that is
|
|
1337
|
+
* the island's doing). The heavy-module and barrel notes leave these out. */
|
|
1338
|
+
const FRAMEWORK = /* @__PURE__ */ new Set([
|
|
1339
|
+
"svelte runtime",
|
|
1340
|
+
"ogygia runtime",
|
|
1341
|
+
"@sveltejs/kit",
|
|
1342
|
+
"devalue",
|
|
1343
|
+
"esm-env"
|
|
1344
|
+
]);
|
|
1345
|
+
function island_js_bytes(row, weights) {
|
|
1346
|
+
if (!weights) return null;
|
|
1347
|
+
let total = 0;
|
|
1348
|
+
let any = false;
|
|
1349
|
+
for (const u of /* @__PURE__ */ new Set([row.module_url, ...row.hints])) {
|
|
1350
|
+
const w = u ? weights[u] : void 0;
|
|
1351
|
+
if (w === void 0) continue;
|
|
1352
|
+
any = true;
|
|
1353
|
+
total += w;
|
|
1354
|
+
}
|
|
1355
|
+
return any ? total : null;
|
|
1356
|
+
}
|
|
1357
|
+
/** an inert island whose lake saves less than this (bytes) is a note, not a warning */
|
|
1358
|
+
const LAKE_WARN_BYTES = 10240;
|
|
1359
|
+
/**
|
|
1360
|
+
* WHAT MAKING SOME ISLANDS LAKES SAVES: the bytes of their closures (each chunk once) that no other
|
|
1361
|
+
* waking island on the page loads — a shared chunk (the framework's runtime) another island needs
|
|
1362
|
+
* downloads anyway. `loads`: their closures, each chunk once. Null without the build's weights.
|
|
1363
|
+
*/
|
|
1364
|
+
function lake_saving(them, rows, weights) {
|
|
1365
|
+
if (!weights) return null;
|
|
1366
|
+
const going = new Set(them.map((r) => r.entry));
|
|
1367
|
+
const kept = /* @__PURE__ */ new Set();
|
|
1368
|
+
for (const r of rows) if (r.wake !== "none" && !going.has(r.entry)) {
|
|
1369
|
+
for (const u of [r.module_url, ...r.hints]) if (u) kept.add(u);
|
|
1370
|
+
}
|
|
1371
|
+
const theirs = /* @__PURE__ */ new Set();
|
|
1372
|
+
for (const r of them) for (const u of [r.module_url, ...r.hints]) if (u) theirs.add(u);
|
|
1373
|
+
let loads = 0;
|
|
1374
|
+
let saves = 0;
|
|
1375
|
+
let any = false;
|
|
1376
|
+
for (const u of theirs) {
|
|
1377
|
+
const w = weights[u];
|
|
1378
|
+
if (w === void 0) continue;
|
|
1379
|
+
any = true;
|
|
1380
|
+
loads += w;
|
|
1381
|
+
if (!kept.has(u)) saves += w;
|
|
1382
|
+
}
|
|
1383
|
+
return any ? {
|
|
1384
|
+
loads,
|
|
1385
|
+
saves
|
|
1386
|
+
} : null;
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* WHAT ONLY EACH ISLAND NEEDS: of an island's closure (its entry and chunks), the bytes no other
|
|
1390
|
+
* waking island on the page uses — what dropping (or deferring) that island would really save.
|
|
1391
|
+
* `island_js_bytes` counts shared chunks once per island; this counts each once, where it belongs.
|
|
1392
|
+
* Keyed by entry (copies of one island are one closure). Null without the build's weights.
|
|
1393
|
+
*/
|
|
1394
|
+
function island_js_unique(rows, weights) {
|
|
1395
|
+
if (!weights) return null;
|
|
1396
|
+
const waking = rows.filter((r) => r.wake !== "none");
|
|
1397
|
+
const users = /* @__PURE__ */ new Map();
|
|
1398
|
+
for (const r of waking) for (const u of /* @__PURE__ */ new Set([r.module_url, ...r.hints])) if (u) (users.get(u) ?? users.set(u, /* @__PURE__ */ new Set()).get(u)).add(r.entry);
|
|
1399
|
+
const out = /* @__PURE__ */ new Map();
|
|
1400
|
+
for (const r of waking) {
|
|
1401
|
+
if (out.has(r.entry)) continue;
|
|
1402
|
+
let only = 0;
|
|
1403
|
+
for (const u of /* @__PURE__ */ new Set([r.module_url, ...r.hints])) if (u && users.get(u)?.size === 1) only += weights[u] ?? 0;
|
|
1404
|
+
out.set(r.entry, only);
|
|
1405
|
+
}
|
|
1406
|
+
return out;
|
|
1407
|
+
}
|
|
1408
|
+
/** ogygia-specific findings: the seed explained, devalue culprits, the wake advisor, hole economics,
|
|
1409
|
+
* island JS weight and the browser's own hydration timings. */
|
|
1410
|
+
function ogygia_findings(og, meta, extras, info, warn) {
|
|
1411
|
+
const islands = group_islands(island_rows_of(meta));
|
|
1412
|
+
const names = (list, max = 3) => list.length <= max ? list.join(", ") : `${list.slice(0, max).join(", ")} and ${list.length - max} more`;
|
|
1413
|
+
const renders = meta.requests.filter((r) => r.internal && r.og?.island_rows?.length).map((r) => r.og.island_rows);
|
|
1414
|
+
if (renders.length >= 2) {
|
|
1415
|
+
const kept = (rows) => rows.map((r) => ({
|
|
1416
|
+
entry: r.entry,
|
|
1417
|
+
fp: r.fp,
|
|
1418
|
+
...r.canonical !== void 0 ? { props: r.canonical } : {}
|
|
1419
|
+
}));
|
|
1420
|
+
const moved = fp_compare(kept(renders[0]), kept(renders[renders.length - 1]));
|
|
1421
|
+
if (moved.length) {
|
|
1422
|
+
const name_of = (entry) => island_name(renders[0].find((r) => r.entry === entry) ?? entry);
|
|
1423
|
+
const m = moved[0];
|
|
1424
|
+
const whose = moved.length === 1 ? "its" : `${name_of(m.entry)}'s`;
|
|
1425
|
+
const said = m.path ? `${whose} prop \`${m.path}\` was ${m.was}, then ${m.now}` : m.was !== void 0 ? `${whose} props went from …${m.was}… to …${m.now}…` : `${whose} props changed`;
|
|
1426
|
+
info("fp-unstable", `${names(moved.map((x) => name_of(x.entry)))} rendered with a different fingerprint on the profiler's renders of the same page: ${said}${moved.length > 1 ? ` (and ${moved.length - 1} more)` : ""}. If the page's data did not change between them, that value is made fresh on every render.`, { fix: "The router compares fingerprints to keep a live island across a navigation, and the fingerprint is part of the page’s bytes: an island whose props change on every render is patched on every navigation, and a cache keyed on the HTML (a CDN, a post-render cache, a postrender store, an ETag) misses every time. Make the value the same for the same inputs: take it from load data, compute it in the browser (an effect), or leave it out of the props." });
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (og.seed && og.seed_bytes > 0) {
|
|
1430
|
+
const shipped = og.seed.keys.filter((k) => k.shipped);
|
|
1431
|
+
const top = shipped[0];
|
|
1432
|
+
if (og.seed.whole_by.length) warn("seed-whole", `${names(og.seed.whole_by.map(island_name))} read${og.seed.whole_by.length === 1 ? "s" : ""} page.data whole, so every key ships in the seed (${fmt_kb(og.seed_bytes)}${top ? `, the biggest is ${top.key} at ${fmt_kb(top.bytes)}` : ""}).`, { fix: "Read the keys by name (page.data.catalog, not a spread or a loop over page.data) so seed shaping can drop the rest, or pass the values in as props." });
|
|
1433
|
+
else if (top && top.bytes >= 20480) {
|
|
1434
|
+
const why = top.reason === "read" ? `read by ${names(top.readers.map(island_name))}` : top.reason === "referenced" ? `props of ${names(top.referenced_by.map(island_name))} point into it` : "shipped";
|
|
1435
|
+
info("seed-explainer", `The seed's biggest key is ${top.key} (${fmt_kb(top.bytes)} of ${fmt_kb(og.seed_bytes)}): ${why}.` + (shipped.length > 1 ? ` ${shipped.length} keys ship in all.` : ""), { fix: top.reason === "read" ? "If the island needs only part of it, return that part from load under its own key, or pass it as a prop." : "A referenced key ships once and the props point into it — that is the cheap shape; trim the key itself if it is bigger than the page needs." });
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (og.seed_bytes > 0 && !og.seed_json && og.seed_culprit) warn("seed-devalue", `The seed (${fmt_kb(og.seed_bytes)}) left the JSON lane because of ${og.seed_culprit} — devalue writes and revives it several times slower than JSON.parse.`, { fix: "Send that value as a plain string or number (a Date as toISOString(), a Map as an object), or keep it out of page.data." });
|
|
1439
|
+
const devalued = islands.filter((r) => !r.json && r.culprit).sort((x, y) => y.props_bytes - x.props_bytes);
|
|
1440
|
+
if (devalued.length) {
|
|
1441
|
+
const d = devalued[0];
|
|
1442
|
+
(d.props_bytes >= 2048 ? warn : info)("props-devalue", `${island_name(d)}'s props (${fmt_kb(d.props_bytes)}${d.count > 1 ? `, ×${d.count}` : ""}) use devalue because of ${d.culprit}` + (devalued.length > 1 ? `; ${devalued.length - 1} more island${devalued.length > 2 ? "s" : ""} likewise.` : "."), { fix: "JSON props parse on the fast lane in the browser: pass the value as a string or number, or derive it inside the island." });
|
|
1443
|
+
}
|
|
1444
|
+
const inert = islands.filter((r) => r.interactivity && r.interactivity.files > 0 && r.wake !== "none" && r.interactivity.handlers + r.interactivity.state + r.interactivity.effects + r.interactivity.binds + r.interactivity.actions + (r.interactivity.remotes ?? 0) + (r.interactivity.awaits ?? 0) + (r.interactivity.shared ?? 0) === 0);
|
|
1445
|
+
if (inert.length) {
|
|
1446
|
+
const lake = lake_saving(inert, islands, extras.weights);
|
|
1447
|
+
const they = inert.length === 1 ? "it loads" : "they load";
|
|
1448
|
+
(lake && lake.loads && lake.saves < LAKE_WARN_BYTES ? info : warn)("wake-inert", `${names(inert.map(island_name))} wake${inert.length === 1 ? "s" : ""} (${names([...new Set(inert.map((r) => r.wake))])}) but the build found no event handlers, $state, $effect, bind:, use:, remote function, await or shared state in ${inert.length === 1 ? "its" : "their"} components` + (!lake || !lake.loads ? "." : lake.saves >= lake.loads ? ` — ${fmt_kb(lake.saves)} of JS loads for markup that never changes.` : lake.saves > 0 ? ` — ${they} ${fmt_kb(lake.loads)} of JS for markup that never changes; ${fmt_kb(lake.saves)} of it is ${inert.length === 1 ? "its" : "theirs"} alone (the rest the page's other islands load too), what a lake saves.` : ` — ${they} ${fmt_kb(lake.loads)} of JS for markup that never changes, all of it shared with the page's other islands: a lake saves ${inert.length === 1 ? "its" : "their"} hydration, not bytes.`), { fix: "Ship them as lakes (wake: 'none'): the server markup stays, the module never downloads." });
|
|
1449
|
+
}
|
|
1450
|
+
const crowds = islands.filter((r) => r.count >= 10 && (r.wake === "load" || r.wake === "idle" || r.wake === "visible"));
|
|
1451
|
+
for (const c of crowds.slice(0, 2)) info("wake-crowd", `${island_name(c)} has ${c.count} copies on the page, each waking on ${c.wake} with its own ${fmt_kb(Math.round(c.props_bytes / Math.max(c.variants ?? 1, 1)))} of props.`, { fix: "One island around the list hydrates once; or wake: 'interaction' so a copy pays only when touched." });
|
|
1452
|
+
if (extras.weights) {
|
|
1453
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1454
|
+
let total = 0;
|
|
1455
|
+
let heaviest = null;
|
|
1456
|
+
for (const r of islands) {
|
|
1457
|
+
if (r.wake === "none") continue;
|
|
1458
|
+
const b = island_js_bytes(r, extras.weights) ?? 0;
|
|
1459
|
+
if (!heaviest || b > heaviest.bytes) heaviest = {
|
|
1460
|
+
row: r,
|
|
1461
|
+
bytes: b
|
|
1462
|
+
};
|
|
1463
|
+
for (const u of [r.module_url, ...r.hints]) {
|
|
1464
|
+
if (!u || seen.has(u)) continue;
|
|
1465
|
+
seen.add(u);
|
|
1466
|
+
total += extras.weights[u] ?? 0;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const unique = island_js_unique(islands, extras.weights);
|
|
1470
|
+
let mine = null;
|
|
1471
|
+
if (unique) {
|
|
1472
|
+
for (const r of islands) if (r.wake !== "none" && (!mine || (unique.get(r.entry) ?? 0) > mine.bytes)) mine = {
|
|
1473
|
+
row: r,
|
|
1474
|
+
bytes: unique.get(r.entry) ?? 0
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
if (total >= 307200 && heaviest) {
|
|
1478
|
+
const pick = mine && mine.bytes >= 20480 ? mine : null;
|
|
1479
|
+
warn("islands-js-heavy", `The page's islands load ${fmt_kb(total)} of JS in all (${seen.size} modules)` + (pick ? `; ${fmt_kb(pick.bytes)} of it is needed by ${island_name(pick.row)} alone` + ((island_js_bytes(pick.row, extras.weights) ?? 0) - pick.bytes >= 1024 ? ` (its whole closure is ${fmt_kb(island_js_bytes(pick.row, extras.weights) ?? 0)}; the rest is shared with other islands)` : "") + " — deferring or dropping it saves that much." : `; ${island_name(heaviest.row)}'s closure is the largest (${fmt_kb(heaviest.bytes)}), but most of it is shared with other islands: no single island's removal saves much.`), { fix: "Open the Islands table: a heavy closure is usually one import (a date or i18n library, a whole component kit) reachable from the island — move it server-side or behind a dynamic import." });
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (extras.heavy && islands.length) {
|
|
1483
|
+
const found = /* @__PURE__ */ new Map();
|
|
1484
|
+
for (const r of islands) {
|
|
1485
|
+
if (r.wake === "none") continue;
|
|
1486
|
+
let own = 0;
|
|
1487
|
+
const by = /* @__PURE__ */ new Map();
|
|
1488
|
+
for (const u of /* @__PURE__ */ new Set([r.module_url, ...r.hints])) {
|
|
1489
|
+
const h = u ? extras.heavy[u] : void 0;
|
|
1490
|
+
if (!h) continue;
|
|
1491
|
+
own += h.total;
|
|
1492
|
+
for (const m of h.top) if (FRAMEWORK.has(m.name)) own -= m.bytes;
|
|
1493
|
+
else by.set(m.name, (by.get(m.name) ?? 0) + m.bytes);
|
|
1494
|
+
}
|
|
1495
|
+
const top = [...by].sort((a, b) => b[1] - a[1])[0];
|
|
1496
|
+
if (!top || own <= 0) continue;
|
|
1497
|
+
const [name, bytes] = top;
|
|
1498
|
+
if (bytes < 20480 || bytes < own * .4) continue;
|
|
1499
|
+
const prev = found.get(name);
|
|
1500
|
+
if (!prev || bytes / own > prev.bytes / prev.of) found.set(name, {
|
|
1501
|
+
row: r,
|
|
1502
|
+
bytes,
|
|
1503
|
+
of: own
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
if (found.size) info("island-heavy-module", [...found].sort((a, b) => b[1].bytes - a[1].bytes).slice(0, 3).map(([name, f]) => `${name.split("/").pop()} is ${Math.round(f.bytes / f.of * 100)}% of ${island_name(f.row)}'s own code (${fmt_kb(f.bytes)} of ${fmt_kb(f.of)}, before minifying)`).join("; ") + ".", { fix: "That one module is where the island’s weight is: split it (one module per piece: an icon, a locale), load it when it is needed, or render it on the server." });
|
|
1507
|
+
}
|
|
1508
|
+
if (extras.barrels && islands.length) {
|
|
1509
|
+
const found = /* @__PURE__ */ new Map();
|
|
1510
|
+
for (const r of islands) {
|
|
1511
|
+
if (r.wake === "none") continue;
|
|
1512
|
+
for (const u of /* @__PURE__ */ new Set([r.module_url, ...r.hints])) for (const b of (u ? extras.barrels[u] : void 0) ?? []) {
|
|
1513
|
+
if (FRAMEWORK.has(b.name)) continue;
|
|
1514
|
+
const prev = found.get(b.name);
|
|
1515
|
+
if (!prev || b.fanout > prev.fanout) found.set(b.name, {
|
|
1516
|
+
row: r,
|
|
1517
|
+
fanout: b.fanout
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (found.size) warn("island-barrel", [...found].sort((a, b) => b[1].fanout - a[1].fanout).slice(0, 3).map(([name, f]) => `${island_name(f.row)} ships a barrel whole: ${name}, and the ${f.fanout} modules behind it`).join("; ") + ". A Svelte component is never side-effect free to the bundler, so what the island does not use ships anyway.", { fix: "Import what the island uses from its own file, or turn on ogygia({ barrels }). If it is on, the barrel has side effects of its own (a statement besides its re-exports), or the build log says why it was skipped." });
|
|
1522
|
+
}
|
|
1523
|
+
const econ = hole_economics(meta);
|
|
1524
|
+
const rows = og.hole_rows ?? [];
|
|
1525
|
+
const by_id = new Map(rows.map((h) => [h.id, h]));
|
|
1526
|
+
const label = (id) => {
|
|
1527
|
+
const h = by_id.get(id);
|
|
1528
|
+
return h ? hole_label(h) : id;
|
|
1529
|
+
};
|
|
1530
|
+
for (const e of econ.values()) {
|
|
1531
|
+
const total = e.hit + e.miss + e.none;
|
|
1532
|
+
if (e.ttl > 0 && total >= 2 && e.hit === 0) warn("hole-cache-cold", `The hole ${label(e.id)} has maxAge ${e.ttl}s but its cache never hit in ${total} requests (${fmt_ms(e.ms / total)} ms each).`, { fix: "The cache key carries the props and the session seal: per-visitor props (a user id, a timestamp) make every key unique. Pass only what the hole renders from." });
|
|
1533
|
+
else if (e.ttl > 0 && e.hit > 0) info("hole-cache", `The hole ${label(e.id)}: ${e.hit} of ${total} requests served from the render cache (maxAge ${e.ttl}s).`);
|
|
1534
|
+
}
|
|
1535
|
+
if (rows.filter((h) => h.ttl === 0).length && econ.size) {
|
|
1536
|
+
const slow = [...econ.values()].filter((e) => e.ttl === 0 && e.ms / Math.max(e.hit + e.miss + e.none, 1) >= 20);
|
|
1537
|
+
if (slow.length) info("hole-uncached", `${slow.length} hole${slow.length === 1 ? "" : "s"} render${slow.length === 1 ? "s" : ""} fresh on every visit: ${names(slow.map((e) => `${label(e.id)} at ${fmt_ms(e.ms / Math.max(e.hit + e.miss + e.none, 1))} ms`))}.`, { fix: "Content that is the same for every visitor for a while can take a maxAge (a preset: { render: 'deferred', maxAge: '5m' }): the endpoint then serves the memo." });
|
|
1538
|
+
}
|
|
1539
|
+
const client = extras.client ?? [];
|
|
1540
|
+
if (client.length) {
|
|
1541
|
+
const seen = new Set(client.map((c) => c.entry));
|
|
1542
|
+
for (const r of extras.visit?.regions ?? []) if (r.failed !== void 0 && r.entry) seen.add(r.entry);
|
|
1543
|
+
const path_of_entry = (e) => {
|
|
1544
|
+
try {
|
|
1545
|
+
return new URL(e, "http://x/").pathname;
|
|
1546
|
+
} catch {
|
|
1547
|
+
return e;
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
const vh = extras.visit?.viewport?.[1];
|
|
1551
|
+
const below_regions = vh ? (extras.visit?.regions ?? []).filter((g) => g.top >= vh) : [];
|
|
1552
|
+
const below_fps = new Set(below_regions.map((g) => g.fp));
|
|
1553
|
+
const below = /* @__PURE__ */ new Set([...below_regions.filter((g) => g.entry).map((g) => path_of_entry(g.entry)), ...island_rows_of(meta).filter((r) => below_fps.has(r.fp)).map((r) => path_of_entry(r.entry))]);
|
|
1554
|
+
const placed = !!vh && (extras.visit?.regions?.length ?? 0) > 0;
|
|
1555
|
+
const unplaced = placed ? [] : islands.filter((r) => r.wake === "visible" && !seen.has(r.entry));
|
|
1556
|
+
const silent = islands.filter((r) => (r.wake === "load" || r.wake === "idle" || r.wake === "visible" && placed && !below.has(path_of_entry(r.entry))) && !seen.has(r.entry));
|
|
1557
|
+
if (unplaced.length) info("never-hydrated", `${names(unplaced.map((r) => `${island_name(r)} (${r.count > 1 ? `${r.count} copies, ` : ""}wake: visible)`))} never reported waking, while ${client.length} other island${client.length === 1 ? "" : "s"} did. No visit said where ${unplaced.length === 1 ? "it sits" : "they sit"} on the page, so whether a visitor ever scrolled ${unplaced.length === 1 ? "it" : "them"} into view is unknown: a visit from a browser carrying the profiler's key tells.`);
|
|
1558
|
+
if (silent.length) warn("never-hydrated", `${names(silent.map((r) => `${island_name(r)} (${r.count > 1 ? `${r.count} copies, ` : ""}wake: ${r.wake})`))} never reported hydrating in your visits, while ${client.length} other island${client.length === 1 ? "" : "s"} did.`, { fix: "A 'visible' island that never intersects the viewport never wakes: check the page can scroll (a design system's stylesheet can pin the body) and that the island is not hidden. A 'load' island that stays silent threw on wake: the browser console has it." });
|
|
1559
|
+
}
|
|
1560
|
+
const broken = client.filter((c) => c.recovered > 0);
|
|
1561
|
+
if (broken.length) {
|
|
1562
|
+
const fps_of = (entry) => new Set(island_rows_of(meta).filter((r) => r.entry === entry).map((r) => r.fp));
|
|
1563
|
+
const seen_of = (c) => {
|
|
1564
|
+
const fps = fps_of(c.entry);
|
|
1565
|
+
return (extras.visit?.islands ?? []).filter((i) => i.recovered && (fps.has(i.fp) || i.entry === c.entry));
|
|
1566
|
+
};
|
|
1567
|
+
const all_same = broken.filter((c) => {
|
|
1568
|
+
const seen = seen_of(c);
|
|
1569
|
+
return seen.length > 0 && seen.every((i) => !i.changed && !i.healed && i.ssr_bytes !== void 0);
|
|
1570
|
+
});
|
|
1571
|
+
const rewired = all_same.filter((c) => seen_of(c).some((i) => i.markers));
|
|
1572
|
+
if (rewired.length) {
|
|
1573
|
+
const one = rewired.length === 1;
|
|
1574
|
+
const m = seen_of(rewired[0]).find((i) => i.markers).markers;
|
|
1575
|
+
const n = rewired.reduce((s, c) => s + c.recovered, 0);
|
|
1576
|
+
warn("hydration-mismatch", `${names(rewired.map(island_name))} discarded ${one ? "its" : "their"} server-rendered DOM and re-rendered in the browser (${n} time${n === 1 ? "" : "s"} seen): ${one ? "its" : "their"} markup arrived with Svelte's hidden block markers unpaired (${m[0]} opening, ${m[1]} closing), and a render always pairs them. Something rewrote ${one ? "it" : "them"} between the server and the browser; the markup the component drew is the same.`, { fix: "Find the rewrite: a transformPageChunk or HTML middleware after the render, an edge rewriter, or a proxy that strips HTML comments. Keep it out of ogygia-region subtrees (or run it before ogygia’s render), and let comments through." });
|
|
1577
|
+
}
|
|
1578
|
+
const same = all_same.filter((c) => !rewired.includes(c));
|
|
1579
|
+
if (same.length) {
|
|
1580
|
+
const one = same.length === 1;
|
|
1581
|
+
const n = same.reduce((s, c) => s + c.recovered, 0);
|
|
1582
|
+
warn("hydration-mismatch", `${names(same.map(island_name))} discarded ${one ? "its" : "their"} server-rendered DOM and re-rendered in the browser (${n} time${n === 1 ? "" : "s"} seen) — and drew the same markup ${one ? "it" : "they"} threw away: only Svelte's hidden block markers disagreed. ${one ? "Its" : "Their"} code drew no other tree: the island's host and its server render disagree on structure (a wrapper of ogygia's), or a block ({#if}, {#each}) took another branch to the same output.`, { fix: "Report it to ogygia with this page and how the island is placed (keep:, inside a snippet, nested in another island): a host that hydrates in another shape than it renders is ogygia's bug. If a block's two branches draw the same thing, give them one branch." });
|
|
1583
|
+
}
|
|
1584
|
+
const rest = broken.filter((c) => !all_same.includes(c));
|
|
1585
|
+
const total = rest.reduce((s, c) => s + c.recovered, 0);
|
|
1586
|
+
const lines = rest.flatMap((c) => extras.hazards?.[c.entry] ?? []);
|
|
1587
|
+
if (rest.length) warn("hydration-mismatch", `${names(rest.map(island_name))} discarded ${rest.length === 1 ? "its" : "their"} server-rendered DOM and re-rendered in the browser (${total} time${total === 1 ? "" : "s"} seen): the markup the browser found was not what the server sent.` + (lines.length ? ` ${hazard_words(lines)}` : ""), {
|
|
1588
|
+
fix: lines.length ? `${hazard_fix(lines)} If none of them is it, something edits the HTML between the render and the wake: a post-SSR pass, a script that runs before the runtime, a comment-stripping proxy.` : "Something edits the HTML between the render and the wake — a post-SSR pass (a design-system renderer, a DSD injector), a script that runs before the runtime, a comment-stripping proxy. Keep it out of ogygia-region subtrees, or run it before ogygia’s render.",
|
|
1589
|
+
...lines[0] ? {
|
|
1590
|
+
file: lines[0].file,
|
|
1591
|
+
line: lines[0].line
|
|
1592
|
+
} : {}
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
if (client.length) {
|
|
1596
|
+
const slow = [...client].sort((x, y) => y.p50_ms - x.p50_ms)[0];
|
|
1597
|
+
const rows = island_rows_of(meta);
|
|
1598
|
+
const copies_of = (entry) => rows.filter((r) => r.entry === entry).reduce((s, r) => s + (r.count || 1), 0);
|
|
1599
|
+
const copies = client.reduce((s, c) => s + copies_of(c.entry), 0);
|
|
1600
|
+
const n_slow = copies_of(slow.entry);
|
|
1601
|
+
const fps = new Set(rows.filter((r) => r.entry === slow.entry).map((r) => r.fp));
|
|
1602
|
+
const mine = (extras.visit?.islands ?? []).filter((i) => fps.has(i.fp) && i.done >= i.t0);
|
|
1603
|
+
const med = (xs) => xs.length ? [...xs].sort((a, b) => a - b)[xs.length >> 1] : 0;
|
|
1604
|
+
const load = mine.length ? med(mine.map((i) => i.loaded - i.t0)) : slow.load_p50_ms;
|
|
1605
|
+
const wait = mine.length ? med(mine.map((i) => i.turn !== void 0 ? i.turn - i.loaded : 0)) : 0;
|
|
1606
|
+
const step = mine.length ? med(mine.map((i) => Math.max(i.done, i.fx ?? 0) - (i.turn ?? i.loaded))) : Math.max(0, slow.p50_ms - slow.load_p50_ms);
|
|
1607
|
+
const whole = load + wait + step || slow.p50_ms;
|
|
1608
|
+
const why = load >= whole * .6 ? "load" : wait >= whole * .5 ? "wait" : "step";
|
|
1609
|
+
let behind = "";
|
|
1610
|
+
let behind_name = "";
|
|
1611
|
+
if (why === "wait" && mine.length) {
|
|
1612
|
+
const one = [...mine].sort((a, b) => (a.turn ?? a.loaded) - a.loaded - ((b.turn ?? b.loaded) - b.loaded))[mine.length >> 1];
|
|
1613
|
+
const w0 = one.loaded;
|
|
1614
|
+
const w1 = one.turn ?? one.loaded;
|
|
1615
|
+
const name_of = new Map(rows.map((r) => [r.fp, island_name(r)]));
|
|
1616
|
+
const by = /* @__PURE__ */ new Map();
|
|
1617
|
+
for (const o of extras.visit?.islands ?? []) {
|
|
1618
|
+
if (o === one || o.turn === void 0) continue;
|
|
1619
|
+
const ran = Math.min(o.done, w1) - Math.max(o.turn, w0);
|
|
1620
|
+
if (ran > 0) by.set(name_of.get(o.fp) ?? o.fp.slice(0, 8), (by.get(name_of.get(o.fp) ?? o.fp.slice(0, 8)) ?? 0) + ran);
|
|
1621
|
+
}
|
|
1622
|
+
const top = [...by].sort((a, b) => b[1] - a[1])[0];
|
|
1623
|
+
if (top && top[1] >= (w1 - w0) * .3) {
|
|
1624
|
+
behind_name = top[0];
|
|
1625
|
+
behind = `, most of it behind ${top[0]} (${fmt_ms(top[1])} ms of hydrating)`;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const slow_wake = rows.find((r) => r.entry === slow.entry)?.wake;
|
|
1629
|
+
info("client-hydrate", `In the browser, ${copies} island${copies === 1 ? "" : "s"} of ${client.length} component${client.length === 1 ? "" : "s"} reported hydration. The slowest, ${island_name(slow)}${n_slow > 1 ? ` (${n_slow} copies)` : ""}, took ${fmt_ms(slow.p50_ms)} ms from wake to hydrated at the median` + (mine.length ? `: ${fmt_ms(load)} ms loading its modules, ${fmt_ms(wait)} ms waiting its turn behind other islands${behind}, ${fmt_ms(step)} ms hydrating.` : load >= slow.p50_ms * .6 ? `, mostly loading its ${fmt_ms(load)} ms of modules.` : "."), { fix: why === "load" ? "Module load dominates: a smaller closure (see the Islands table) or an earlier preload helps more than faster code." : why === "wait" ? behind ? `It waited for its turn: islands hydrate one per task, and ${behind_name} held the queue. Make that hydrate step short (fewer elements, split the island), or wake it later.` : slow_wake === "visible" ? "It waited for its turn: islands hydrate one per task, and many became visible at once (a fast scroll, a tall screen). Make the copies with nothing to click lakes, or render the list as one island." : "It waited for its turn: islands hydrate one per task, so many waking at once queue behind each other. Wake the ones below the first screen when visible, the rest when idle, and make the ones with nothing to click lakes." : "The hydrate step itself is slow: fewer elements per island, or split the island so the interactive part is small." });
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
/** Kit-shaped findings: the load lanes and the parent() chain, universal loads, and the client
|
|
1633
|
+
* router's serialization of load data. */
|
|
1634
|
+
/** ms of devalue work done for Kit's copy of the server load data: each devalue function's own time,
|
|
1635
|
+
* by the share of its call paths that pass through Kit's serializer (`add_node`, data_serializer.js,
|
|
1636
|
+
* or render_response). A function with no recorded paths counts only when it is `uneval` itself. */
|
|
1637
|
+
function kit_load_serialize_ms(a) {
|
|
1638
|
+
let ms = 0;
|
|
1639
|
+
for (const f of a.functions) {
|
|
1640
|
+
if (f.pkg !== "devalue") continue;
|
|
1641
|
+
const stacks = f.stacks ?? [];
|
|
1642
|
+
if (!stacks.length) {
|
|
1643
|
+
if (f.name === "uneval") ms += f.self_ms;
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
let kit = 0;
|
|
1647
|
+
let all = 0;
|
|
1648
|
+
for (const s of stacks) {
|
|
1649
|
+
all += s.ms;
|
|
1650
|
+
if (s.frames.some((fr) => fr.n === "add_node" || fr.n === "render_response" || fr.f.indexOf("data_serializer") !== -1)) kit += s.ms;
|
|
1651
|
+
}
|
|
1652
|
+
if (all > 0) ms += f.self_ms * (kit / all);
|
|
1653
|
+
}
|
|
1654
|
+
return ms;
|
|
1655
|
+
}
|
|
1656
|
+
function kit_findings(a, meta, info, warn) {
|
|
1657
|
+
const tl = a.timeline;
|
|
1658
|
+
if (tl?.chain) {
|
|
1659
|
+
const c = tl.chain;
|
|
1660
|
+
const u = c.parent_use;
|
|
1661
|
+
const names = u ? u.names.join(", ") : "";
|
|
1662
|
+
const used = !u ? "" : u.first_use === null ? ` Nothing after line ${u.line} reads ${names}: the page waited on the layout for nothing.` : u.awaits_before_use > 0 ? ` It takes ${names} from parent() on line ${u.line} but first reads ${u.names.length === 1 ? "it" : "them"} on line ${u.first_use}; the ${u.awaits_before_use} await${u.awaits_before_use === 1 ? "" : "s"} between never needed the layout.` : "";
|
|
1663
|
+
warn("parent-chain", `${c.page} started only after ${c.layout} finished (${fmt_ms(c.serial_ms)} ms later)` + (c.explicit || u ? " — it awaits parent()." : " — Kit runs a page load and its layout loads together unless the page awaits parent().") + used, {
|
|
1664
|
+
fix: u && u.first_use !== null && u.awaits_before_use > 0 ? `Move \`await parent()\` down to just before line ${u.first_use}, where ${names} is first used: the calls above it then run while the layout's load does.` : "Move the await parent() below the page's own fetches (start them first, await parent() after), or pass what the page needs some other way — the two loads then overlap.",
|
|
1665
|
+
...u ? {
|
|
1666
|
+
file: c.page,
|
|
1667
|
+
line: u.line
|
|
1668
|
+
} : {}
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
for (const l of tl?.lanes ?? []) if (l.kind === "universal" && l.wait_ms >= 5) info("universal-load", `${l.file} is a universal load: on this render it waited ${fmt_ms(l.wait_ms)} ms on calls. On a page with the client router it runs again in the browser on every navigation, and its data must be serializable.`, { fix: "If it only needs the server (a database, a secret, a private API), rename it +" + l.level + ".server.ts and the browser never runs it." });
|
|
1672
|
+
const uneval = a.functions.filter((f) => f.pkg === "devalue").sort((x, y) => y.self_ms - x.self_ms);
|
|
1673
|
+
const kit_ms = kit_load_serialize_ms(a);
|
|
1674
|
+
const runs = meta.trigger === "page" ? Math.max(meta.runs?.length ?? 1, 1) : 1;
|
|
1675
|
+
if (uneval.length && kit_ms >= 3 && kit_ms >= a.busy_ms * .03) {
|
|
1676
|
+
const csr_false = meta.requests.some((r) => r.internal && r.og);
|
|
1677
|
+
info("kit-uneval", csr_false ? `Kit serialized this page's server load data: ${fmt_ms(kit_ms / runs)} ms per render (devalue.uneval). On a csr=false page nothing reads that copy — Kit builds it on every request and drops it.` : `Kit serialized the load data for its client router: ${fmt_ms(kit_ms / runs)} ms per render (devalue.uneval). This page has csr on.`, {
|
|
1678
|
+
anchor: `fn:${uneval[0].key}`,
|
|
1679
|
+
fix: csr_false ? "Only less load data helps: return from the server loads (+page.server.ts, +layout.server.ts) only what this page renders. Islands get what they read from ogygia’s own seed, shaped apart from it. The work is Kit’s, not yours." : "Return from the server load only what the page renders: Kit serializes all of it into the HTML for its client router. (csr=false does not make it free — Kit still builds the copy and drops it — so less data is the fix either way.)"
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
const busy = a.busy_ms || 1;
|
|
1683
|
+
const list = a.components.find((c) => (c.calls ?? 0) >= 20 && c.parent && c.total_ms >= busy * .1);
|
|
1684
|
+
if (list) {
|
|
1685
|
+
const n = list.calls ?? 0;
|
|
1686
|
+
info("hot-list", `${list.parent} renders ${n} ${list.name} rows per render, ${fmt_ms(list.total_ms / runs)} ms together (${fmt_ms(list.total_ms / runs / Math.max(n, 1))} ms each).`, {
|
|
1687
|
+
anchor: `comp:${list.parent}`,
|
|
1688
|
+
fix: "The list is the cost, not the row: page it, window it, or defer the part below the fold into a hole."
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
const split = a.components.find((c) => (c.markup_ms ?? 0) + (c.logic_ms ?? 0) >= busy * .15 && (c.markup_ms ?? 0) + (c.logic_ms ?? 0) >= 5);
|
|
1692
|
+
if (split) {
|
|
1693
|
+
const m = (split.markup_ms ?? 0) / runs;
|
|
1694
|
+
const l = (split.logic_ms ?? 0) / runs;
|
|
1695
|
+
const own = m + l;
|
|
1696
|
+
if (m >= own * .7) info("markup-heavy", `${split.name}'s own time is mostly building markup: ${fmt_ms(m)} of ${fmt_ms(own)} ms is Svelte writing its template per render, ${fmt_ms(l)} ms its script.`, {
|
|
1697
|
+
anchor: `comp:${split.name}`,
|
|
1698
|
+
fix: "Fewer elements and attributes per instance help here; the script is not the problem. Static blocks can move into a lake or a prebaked snippet."
|
|
1699
|
+
});
|
|
1700
|
+
else if (l >= own * .7) info("logic-heavy", `${split.name}'s own time is mostly its script: ${fmt_ms(l)} of ${fmt_ms(own)} ms runs code per render, only ${fmt_ms(m)} ms writes markup.`, {
|
|
1701
|
+
anchor: `comp:${split.name}`,
|
|
1702
|
+
fix: "Open the row: the hot function under it is the one to hoist into load or cache per request."
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
/** Cold vs warm per file: what the first render paid over a warm one (module load + compile). */
|
|
1707
|
+
function cold_rows(a, meta) {
|
|
1708
|
+
if (!meta.cold) return [];
|
|
1709
|
+
const runs = runs_of(meta);
|
|
1710
|
+
const warm = new Map(a.files.map((f) => [f.key, f.self_ms / runs]));
|
|
1711
|
+
return meta.cold.files.map((f) => {
|
|
1712
|
+
const w = warm.get(f.file) ?? 0;
|
|
1713
|
+
return {
|
|
1714
|
+
file: f.file,
|
|
1715
|
+
category: f.category,
|
|
1716
|
+
cold_ms: f.ms,
|
|
1717
|
+
warm_ms: round1(w),
|
|
1718
|
+
extra_ms: round1(f.ms - w)
|
|
1719
|
+
};
|
|
1720
|
+
}).filter((r) => r.extra_ms >= .5).sort((x, y) => y.extra_ms - x.extra_ms);
|
|
1721
|
+
}
|
|
1722
|
+
/** The spread of a component across runs: median of the rest against the max, and the cold first run. */
|
|
1723
|
+
function run_spread(runs_ms) {
|
|
1724
|
+
if (runs_ms.length < 2) return null;
|
|
1725
|
+
const sorted = [...runs_ms].sort((x, y) => x - y);
|
|
1726
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
1727
|
+
const max = sorted[sorted.length - 1];
|
|
1728
|
+
const rest = runs_ms.slice(1);
|
|
1729
|
+
const rest_median = [...rest].sort((x, y) => x - y)[Math.floor(rest.length / 2)];
|
|
1730
|
+
return {
|
|
1731
|
+
min: sorted[0],
|
|
1732
|
+
median,
|
|
1733
|
+
max,
|
|
1734
|
+
max_run: runs_ms.indexOf(max) + 1,
|
|
1735
|
+
/** the median of every run after the first: what "warm" costs */
|
|
1736
|
+
after: rest_median,
|
|
1737
|
+
cold: runs_ms[0] >= 5 && runs_ms[0] >= rest_median * 2.5 && max === runs_ms[0]
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
/** Do the runs climb? The page getting slower render by render — what a page that keeps memory, or
|
|
1741
|
+
* a cache that grows without a bound, does to an instance as it ages. The slope is the median of
|
|
1742
|
+
* every pair's slope (one slow run cannot tilt it), and it counts only when most pairs agree: at
|
|
1743
|
+
* least 3 of 4 later runs slower than an earlier one. A cold first run is left out (that is the
|
|
1744
|
+
* cold start, not a climb). Needs 4 runs; null when the runs do not climb. */
|
|
1745
|
+
function run_trend(runs_ms) {
|
|
1746
|
+
let r = runs_ms;
|
|
1747
|
+
if (r.length >= 5 && run_spread([...r])?.cold) r = r.slice(1);
|
|
1748
|
+
const n = r.length;
|
|
1749
|
+
if (n < 4) return null;
|
|
1750
|
+
const slopes = [];
|
|
1751
|
+
let up = 0;
|
|
1752
|
+
let pairs = 0;
|
|
1753
|
+
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
|
|
1754
|
+
slopes.push((r[j] - r[i]) / (j - i));
|
|
1755
|
+
if (r[j] > r[i]) up++;
|
|
1756
|
+
pairs++;
|
|
1757
|
+
}
|
|
1758
|
+
slopes.sort((x, y) => x - y);
|
|
1759
|
+
const slope = slopes[Math.floor(slopes.length / 2)];
|
|
1760
|
+
const agree = up / pairs;
|
|
1761
|
+
const base = Math.min(r[0], r[1]);
|
|
1762
|
+
const rise = slope * (n - 1);
|
|
1763
|
+
if (slope <= 0 || agree < .75 || base <= 0 || rise < Math.max(3, base * .05)) return null;
|
|
1764
|
+
return {
|
|
1765
|
+
from: round1(r[0]),
|
|
1766
|
+
to: round1(r[n - 1]),
|
|
1767
|
+
slope_ms: round1(slope),
|
|
1768
|
+
rise_ms: round1(rise),
|
|
1769
|
+
rise_pct: Math.round(rise / base * 100),
|
|
1770
|
+
agree: Math.round(agree * 100) / 100,
|
|
1771
|
+
runs: n
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
/** Findings from the accuracy round: the upstream's own split of a wait, the cold render, a
|
|
1775
|
+
* component whose runs disagree, and the browser's vitals against the server's render. */
|
|
1776
|
+
function accuracy_findings(a, meta, extras, info, warn) {
|
|
1777
|
+
const runs = runs_of(meta);
|
|
1778
|
+
const told = extras.net.filter((c) => c.ms >= 0 && c.timings?.length).sort((x, y) => y.ms + (y.body_ms ?? 0) - (x.ms + (x.body_ms ?? 0)))[0];
|
|
1779
|
+
if (told && told.ms >= 20) {
|
|
1780
|
+
const sum = told.timings.reduce((s, t) => s + t.ms, 0);
|
|
1781
|
+
const largest = Math.max(0, ...told.timings.map((t) => t.ms));
|
|
1782
|
+
const nested = sum > told.ms;
|
|
1783
|
+
const theirs = Math.min(told.ms, nested ? largest : sum);
|
|
1784
|
+
const parts = told.timings.filter((t) => t.ms > 0).sort((x, y) => y.ms - x.ms).slice(0, 4).map((t) => `${t.desc ?? t.name} ${fmt_ms(t.ms)} ms`).join(", ");
|
|
1785
|
+
const { host, tpl } = path_template(told.url);
|
|
1786
|
+
info("upstream-split", `${told.method} ${host}${tpl} waited ${fmt_ms(told.ms)} ms; its own Server-Timing says ${parts || "nothing measurable"}` + (theirs > 0 ? ` — ${fmt_ms(theirs)} ms of the wait is on their side${nested ? " (the entries overlap, so the largest is taken as the whole)" : ""}, ${fmt_ms(Math.max(0, told.ms - theirs))} ms is the network and their framework.` : "."), { fix: theirs >= told.ms * .6 ? "The wait is theirs: take the biggest entry to whoever owns that service, or cache the response." : "Most of the wait is outside their measured work: the network path, TLS, or a queue in front of them." });
|
|
1787
|
+
}
|
|
1788
|
+
if (meta.cold && meta.runs?.length) {
|
|
1789
|
+
const warm = [...meta.runs].sort((x, y) => x - y)[Math.floor(meta.runs.length / 2)];
|
|
1790
|
+
const rows = cold_rows(a, meta);
|
|
1791
|
+
const extra = rows.reduce((s, r) => s + r.extra_ms, 0);
|
|
1792
|
+
const parts = (meta.cold.owners ?? []).map((o) => {
|
|
1793
|
+
const w = a.owner_runs_ms?.[o.label];
|
|
1794
|
+
const med = w?.length ? [...w].sort((x, y) => x - y)[Math.floor(w.length / 2)] : 0;
|
|
1795
|
+
return {
|
|
1796
|
+
label: o.label,
|
|
1797
|
+
extra: o.ms - med
|
|
1798
|
+
};
|
|
1799
|
+
}).filter((o) => o.extra >= 5).sort((x, y) => y.extra - x.extra).slice(0, 3);
|
|
1800
|
+
const by_part = parts.length ? `; by part of the render: ${parts.map((o) => `${o.label} +${fmt_ms(o.extra)} ms`).join(", ")}` : "";
|
|
1801
|
+
if (meta.cold.ms >= warm * 1.5 && meta.cold.ms - warm >= 50) {
|
|
1802
|
+
const more = meta.cold.ms - warm;
|
|
1803
|
+
const cpu_more = Math.min(more, Math.max(0, meta.cold.busy_ms - a.busy_ms / runs));
|
|
1804
|
+
const wait_more = more - cpu_more;
|
|
1805
|
+
const calls = meta.cold.calls;
|
|
1806
|
+
const waiting = calls === 0 ? `, and ${fmt_ms(wait_more)} ms more waiting with no outbound call in that render: not the network, but the first loads of the modules and files it reads (a dynamic import, a file read), which CPU samples do not show` : `, and ${fmt_ms(wait_more)} ms more waiting (${calls ? `its ${calls} outbound call${calls === 1 ? "" : "s"} opened new connections` : "its connections were new"}: DNS, TLS, empty pools)`;
|
|
1807
|
+
warn("cold-start", `The first render took ${fmt_ms(meta.cold.ms)} ms against ${fmt_ms(warm)} ms warm: ${fmt_ms(cpu_more)} ms more CPU (module load, compile, first-call caches)` + (rows[0] ? `, ${share_word(rows[0].extra_ms, cpu_more)} ${rows[0].file} (${fmt_ms(rows[0].extra_ms)} ms)` : "") + by_part + (wait_more >= 10 ? waiting : "") + ". On a serverless host every cold instance pays this.", { fix: wait_more > cpu_more && calls !== 0 ? "Most of it is the first calls opening their connections: reuse one HTTP agent or client across requests (module scope, keep-alive on), and keep calls to services that are slow to connect out of the first render where you can." : "Fewer and smaller server modules on the page’s path: lazy-import what the render rarely needs, keep heavy libraries out of hooks and layouts, and prefer a warm instance (provisioned concurrency) where the platform offers one." });
|
|
1808
|
+
} else if (rows.length && extra >= 20) info("cold-start", `The first render paid ${fmt_ms(extra)} ms of module load and compile over a warm one, ${share_word(rows[0].extra_ms, extra)} ${rows[0].file} (${fmt_ms(rows[0].extra_ms)} ms)${by_part}.`);
|
|
1809
|
+
}
|
|
1810
|
+
const inst = meta.instance;
|
|
1811
|
+
if (meta.lambda && inst && inst.first_request_ms >= 100) {
|
|
1812
|
+
const first_render = meta.cold && meta.runs?.length ? meta.cold.ms - [...meta.runs].sort((x, y) => x - y)[Math.floor(meta.runs.length / 2)] : 0;
|
|
1813
|
+
info("cold-instance", `This instance took ${fmt_ms(inst.first_request_ms)} ms from its process starting to its first request: Node's own startup ${fmt_ms(inst.node_ms)} ms, the rest loading the server bundle and its imports and the platform handing the request over. Every new instance on AWS Lambda pays that before its first render` + (first_render >= 20 ? `, and the first render then pays about ${fmt_ms(first_render)} ms more to warm up` : "") + `. This one was ${inst.age_s} s old and had served ${inst.requests_before} request${inst.requests_before === 1 ? "" : "s"} before this recording.`, { fix: "Load less at startup: import heavy SDKs where a page needs them (a dynamic import inside the load or the handler), keep them out of hooks.server and root layouts, and use provisioned concurrency where the first visitor after a quiet spell matters." });
|
|
1814
|
+
}
|
|
1815
|
+
const climb = meta.trigger === "page" && meta.runs ? run_trend(meta.runs) : null;
|
|
1816
|
+
if (climb) {
|
|
1817
|
+
const kept = extras.retained && extras.retained.total_bytes >= 262144 ? extras.retained : void 0;
|
|
1818
|
+
const g = extras.growth;
|
|
1819
|
+
const why = kept ? ` One render leaves ${Math.round(kept.total_bytes / 1048576 * 10) / 10} MB alive after a full collection${g ? g.levels_off ? ", and it levels off, so the climb should stop once that cache is full" : ", and it keeps growing, so the climb will not stop" : ""}: the bigger heap makes every collection, and every lookup in what is kept, cost more.` : " Nothing this report measured was left alive by a render, so look for a list, map or cache the page adds to on every request (a memo keyed by something new each time), or a timer or listener it registers again each render.";
|
|
1820
|
+
warn("slower-each-run", `Each render took about ${fmt_ms(climb.slope_ms)} ms longer than the one before: ${fmt_ms(climb.from)} ms → ${fmt_ms(climb.to)} ms over ${climb.runs} runs (+${climb.rise_pct}%).${why} On a long-lived server this keeps climbing until the instance restarts, and the times in this report sit somewhere along the way.`, {
|
|
1821
|
+
fix: kept ? "Bound or release what a render keeps (the Kept per render pattern names the line), then profile again: the runs should come out flat." : "Profile again with more renders: if the climb holds, find what grows per request; if it flattens, it was a cache warming.",
|
|
1822
|
+
...kept?.sites[0]?.url ? {
|
|
1823
|
+
file: kept.sites[0].url,
|
|
1824
|
+
line: kept.sites[0].line
|
|
1825
|
+
} : {}
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
const heap0 = extras.mem[0]?.heap_used;
|
|
1829
|
+
const kept_each = extras.retained?.total_bytes ?? 0;
|
|
1830
|
+
if (meta.trigger === "page" && heap0 !== void 0 && heap0 >= 256 && kept_each >= 8388608 && !extras.growth?.levels_off) {
|
|
1831
|
+
const kept_mb = kept_each / 1048576;
|
|
1832
|
+
const worth = Math.round(heap0 / kept_mb);
|
|
1833
|
+
if (worth >= 6) warn("heap-filled-before", `This profile started with ${Math.round(heap0)} MB of heap in use: about ${worth} renders' worth of what this page keeps alive (${Math.round(kept_mb * 10) / 10} MB each), left by earlier requests. A bigger heap makes every render slower, so the times here are higher than a freshly started server shows, and the next profile will read higher still.`, { fix: "Fix what a render keeps (the Kept per render pattern names the line). Until then, compare numbers from freshly started servers only: restart, then profile once." });
|
|
1834
|
+
}
|
|
1835
|
+
if (runs > 1) {
|
|
1836
|
+
const spread = a.components.map((c) => ({
|
|
1837
|
+
c,
|
|
1838
|
+
s: c.runs_ms ? run_spread(c.runs_ms) : null
|
|
1839
|
+
})).filter((x) => !!x.s && x.s.max >= 5).sort((x, y) => y.s.max - y.s.median - (x.s.max - x.s.median))[0];
|
|
1840
|
+
if (spread && spread.s.cold) info("component-cold-run", `${spread.c.name} took ${fmt_ms(spread.s.max)} ms in the first render and ${fmt_ms(spread.s.after)} ms after: a cache that was cold, or a lazy import — not a slow component.`, {
|
|
1841
|
+
anchor: `comp:${spread.c.name}`,
|
|
1842
|
+
fix: "Warm it at startup, or accept it: only the first request after a deploy pays this."
|
|
1843
|
+
});
|
|
1844
|
+
else if (spread && spread.s.max >= spread.s.median * 3 && spread.s.max - spread.s.median >= 10) warn("component-variance", `${spread.c.name} is ${fmt_ms(spread.s.median)} ms in most renders but ${fmt_ms(spread.s.max)} ms in run ${spread.s.max_run}: something it waits on or caches is not steady.`, {
|
|
1845
|
+
anchor: `comp:${spread.c.name}`,
|
|
1846
|
+
fix: "Open the row: the per-run column shows the spread; a cache with a short TTL, a GC pause, or a shared upstream are the usual causes."
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
const marks = extras.client_marks ?? [];
|
|
1850
|
+
if (marks.length) {
|
|
1851
|
+
const slow = [...marks].sort((x, y) => y.p50_ms - x.p50_ms)[0];
|
|
1852
|
+
const failed = marks.filter((m) => m.errors > 0);
|
|
1853
|
+
info("client-mark", `In the browser the app marked ${marks.length} thing${marks.length === 1 ? "" : "s"}: the slowest is ${slow.name} at ${fmt_ms(slow.p50_ms)} ms (p50 of ${slow.n})` + (failed.length ? `; ${failed.map((m) => m.name).join(", ")} failed.` : "."), { fix: slow.p50_ms >= 300 ? `${slow.name} is a wait the visitor feels after the HTML arrived: it is not the server render, take it to whatever ${slow.name} times.` : void 0 });
|
|
1854
|
+
}
|
|
1855
|
+
const v = extras.vitals;
|
|
1856
|
+
if (v && v.lcp !== null) {
|
|
1857
|
+
const server = meta.runs?.length ? [...meta.runs].sort((x, y) => x - y)[Math.floor(meta.runs.length / 2)] : meta.request?.ms ?? 0;
|
|
1858
|
+
const ttfb = v.ttfb ?? 0;
|
|
1859
|
+
if (v.lcp - ttfb >= Math.max(500, ttfb) && server > 0) {
|
|
1860
|
+
const own = extras.visit;
|
|
1861
|
+
const font = lcp_font(own);
|
|
1862
|
+
const after = (own?.paints?.lcp_url ? vital_parts({
|
|
1863
|
+
vitals: {},
|
|
1864
|
+
visit: {
|
|
1865
|
+
nav: own.nav,
|
|
1866
|
+
paints: own.paints,
|
|
1867
|
+
resources: own.resources
|
|
1868
|
+
},
|
|
1869
|
+
islands: [],
|
|
1870
|
+
firsts: [],
|
|
1871
|
+
shifts: [],
|
|
1872
|
+
longtasks: []
|
|
1873
|
+
}, "lcp") : null)?.filter((x) => x.key !== "ttfb") ?? [];
|
|
1874
|
+
const top = after.length ? after.reduce((a, b) => b.ms > a.ms ? b : a) : null;
|
|
1875
|
+
const gap = (own?.paints?.lcp ?? v.lcp) - ttfb;
|
|
1876
|
+
const lcp_file = own?.paints?.lcp_url ? own.paints.lcp_url.slice(own.paints.lcp_url.lastIndexOf("/") + 1).split("?")[0] : "";
|
|
1877
|
+
const part = !font && top && top.ms >= gap / 2 ? top : null;
|
|
1878
|
+
const PART_WORDS = {
|
|
1879
|
+
delay: [`most of it (${fmt_ms(part?.ms ?? 0)} ms) before the browser began fetching the largest paint, ${lcp_file}.`, part?.key === "delay" ? late_found(own?.paints, own?.resources.find((r) => r.url === own?.paints?.lcp_url), own?.islands ?? [], (fp) => {
|
|
1880
|
+
const r = fp ? island_rows_of(meta).find((x) => x.fp === fp) : void 0;
|
|
1881
|
+
return r ? island_name(r) : "its island";
|
|
1882
|
+
}) : ""],
|
|
1883
|
+
load: [`most of it (${fmt_ms(part?.ms ?? 0)} ms) downloading the largest paint, ${lcp_file}.`, "The largest paint is slow to download: make it smaller (a modern format, sized to how it shows, `srcset`) and serve it from close by. The server is not the bottleneck here."],
|
|
1884
|
+
render: [`most of it (${fmt_ms(part?.ms ?? 0)} ms) after the largest paint, ${lcp_file}, had arrived: something held its paint.`, "The largest paint was ready but did not paint: render-blocking styles or scripts, or an island that shows it on waking. Render it with the server HTML."]
|
|
1885
|
+
};
|
|
1886
|
+
const words = part ? PART_WORDS[part.key] : void 0;
|
|
1887
|
+
const rivals = font || part?.key === "load" || part?.key === "delay" ? lcp_rivals(own) : null;
|
|
1888
|
+
warn("lcp-gap", `In the browser LCP is ${fmt_ms(v.lcp)} ms while the server answered in ${fmt_ms(ttfb)} ms (TTFB; the render itself ${fmt_ms(server)} ms): ${fmt_ms(v.lcp - ttfb)} ms of the user's wait is after the HTML arrived — ` + (font ? `the largest paint is text that waited for its font '${font.family}' (${font.file}, in at ${fmt_ms(font.end)} ms), invisible until it came.` : words ? words[0] : "assets, fonts, hydration.") + (rivals ? ` Beside it, ${fmt_bytes(rivals.bytes)} of images far below the first screen downloaded (${rivals.files.join(", ")}).` : ""), { fix: (font ? "Give the font's @font-face `font-display: swap` (or `optional`) and preload the file: the text paints with the HTML, and the server is not the bottleneck here." : words ? words[1] : "Make the hero markup static (a lake), preload its image and font, and keep the islands above the fold small: the server is not the bottleneck here.") + (rivals ? " And give the images below the first screen `loading=\"lazy\"`: they took a share of the network the largest paint needed." : "") });
|
|
1889
|
+
} else if (ttfb > 0 && ttfb >= server * 2 && ttfb - server >= 200) {
|
|
1890
|
+
const own = extras.visit?.nav;
|
|
1891
|
+
const ph = own?.phases;
|
|
1892
|
+
const held = extras.doc_request?.ms;
|
|
1893
|
+
let split = "";
|
|
1894
|
+
let where = null;
|
|
1895
|
+
if (ph && held !== void 0) {
|
|
1896
|
+
const connect = (ph.redirect ?? 0) + (ph.worker ?? 0) + (ph.dns ?? 0) + (ph.connect ?? 0) + (ph.tls ?? 0);
|
|
1897
|
+
const before = Math.max(0, (ph.wait ?? 0) - held);
|
|
1898
|
+
const inside = Math.max(0, held - server);
|
|
1899
|
+
where = connect >= before && connect >= inside ? "connect" : before >= inside ? "before" : "inside";
|
|
1900
|
+
split = ` In the report's own visit (TTFB ${fmt_ms(own.res_start)} ms): ${fmt_ms(connect)} ms redirecting and connecting, ${fmt_ms(before)} ms before the server's handler took the request, and ${fmt_ms(inside)} ms inside the handler beyond the render.`;
|
|
1901
|
+
}
|
|
1902
|
+
warn("ttfb-gap", `TTFB in the browser is ${fmt_ms(ttfb)} ms but this server rendered the page in ${fmt_ms(server)} ms: ${fmt_ms(ttfb - server)} ms sits between the two${where ? "." : " — a cold instance, a proxy, or the network."}${split}`, { fix: where === "connect" ? "The connection is the cost: redirects to drop, or a server far from the visitor (serve it from the edge, keep TLS and HTTP modern)." : where === "before" ? "The request waited before this server took it: a proxy or CDN in front, a cold instance starting, or the network. Look at what fronts the server, and at the cold-start section." : where === "inside" ? meta.dev ? "The server held the request past its render: on the dev server a first request compiles the page (profile a build for its real TTFB), or a hook before the render awaits something." : "The server held the request past its render: a hook before or after the render (auth, a session lookup, a redirect check) awaits something. Profile it with the handle, not only the page." : "Look at the cold-start section and at what fronts the server (a CDN, an auth proxy): the render is not where that time goes." });
|
|
1903
|
+
} else info("browser-vitals", `The browser measured TTFB ${v.ttfb === null ? "—" : fmt_ms(v.ttfb) + " ms"}, LCP ${fmt_ms(v.lcp)} ms${v.cls !== null ? `, CLS ${v.cls}` : ""}${v.inp !== null ? `, INP ${fmt_ms(v.inp)} ms` : ""} over ${v.n} visit${v.n === 1 ? "" : "s"}.` + (dock_skewed(extras.visit) ? " The latest visit loaded with the devtools dock open, and its paints may be the dock’s: read the paints from a visit with the dock closed." : ""));
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* The whole profile as one curated JSON object — the agent-facing view. Served
|
|
1908
|
+
* at `<base>/report/<id>.json`. Not the raw V8 profile (that is `/raw`): this is
|
|
1909
|
+
* already analyzed — self/total per component, network attribution, memory, GC,
|
|
1910
|
+
* and the same findings the human report shows.
|
|
1911
|
+
*/
|
|
1912
|
+
/** One stack frame as agents read it: `name (file:line)`. */
|
|
1913
|
+
const frame_text = (fr) => fr.f ? `${fr.n} (${fr.f})` : fr.n;
|
|
1914
|
+
/** Gather the ogygia signals a page score reads — the same numbers the findings above compute, so
|
|
1915
|
+
* the score and the findings never disagree. All safe when a signal is absent (no visits, no server
|
|
1916
|
+
* timing). See {@link page_score}. */
|
|
1917
|
+
function page_score_of(meta, extras) {
|
|
1918
|
+
const islands = group_islands(island_rows_of(meta));
|
|
1919
|
+
const og = [...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;
|
|
1920
|
+
const client = extras.client ?? [];
|
|
1921
|
+
const seen_entry = new Set(client.map((c) => c.entry));
|
|
1922
|
+
const server_ms = meta.runs?.length ? [...meta.runs].sort((x, y) => x - y)[Math.floor(meta.runs.length / 2)] : meta.request?.ms ?? null;
|
|
1923
|
+
const v = extras.vitals;
|
|
1924
|
+
const visit = extras.visit;
|
|
1925
|
+
const pa = extras.assets;
|
|
1926
|
+
const sj = start_js(extras);
|
|
1927
|
+
const assets = pa ? {
|
|
1928
|
+
js: sj.js,
|
|
1929
|
+
js_wire: sj.js_wire,
|
|
1930
|
+
js_files: sj.js_files,
|
|
1931
|
+
lazy_js: Math.max(0, pa.totals.lazy_js - (sj.woke_early?.bytes ?? 0) - (sj.runtime_parts?.bytes ?? 0)),
|
|
1932
|
+
css: pa.totals.css,
|
|
1933
|
+
wire: pa.totals.wire + (sj.runtime?.wire ?? 0) + (sj.runtime_parts?.wire ?? 0),
|
|
1934
|
+
blocking: pa.totals.blocking,
|
|
1935
|
+
blocking_count: pa.totals.blocking_count,
|
|
1936
|
+
top: pa.assets.filter((x) => x.kind === "script" && !x.lazy).slice(0, 4).map((x) => `${asset_name(x.url)} ${fmt_bytes(x.bytes)}${x.contains?.length ? ` (${x.contains.slice(0, 3).join(", ")})` : ""}`).concat(sj.runtime ? [`loaded at runtime ${sj.runtime.island_entries && extras.visit?.holes_answered?.length ? `(islands inside its holes) ${fmt_bytes(sj.runtime.bytes)}` : `by other scripts ${fmt_bytes(sj.runtime.bytes)} (${sj.runtime.by[0].who})`}`] : [])
|
|
1937
|
+
} : null;
|
|
1938
|
+
const inline_script = extras.strip?.by_kind.script ?? pa?.inline.script ?? 0;
|
|
1939
|
+
const data_parts = [
|
|
1940
|
+
["page seed", og?.seed_bytes ?? 0],
|
|
1941
|
+
["island props", og?.tail_bytes ?? 0],
|
|
1942
|
+
["remote seed", og?.remote_seed_bytes ?? 0],
|
|
1943
|
+
["inline script", inline_script]
|
|
1944
|
+
];
|
|
1945
|
+
const data_bytes = data_parts.reduce((s, [, b]) => s + b, 0);
|
|
1946
|
+
const no_longtasks = !!visit?.unsupported?.includes("longtask");
|
|
1947
|
+
const no_shifts = !!visit?.unsupported?.includes("layout-shift");
|
|
1948
|
+
const tbt = no_longtasks ? null : visit?.longtasks?.length ? Math.round(visit.longtasks.reduce((s, t) => s + Math.max(0, t.ms - 50), 0)) : visit ? 0 : null;
|
|
1949
|
+
const unmeasured = {
|
|
1950
|
+
...no_longtasks ? { tbt: "the browser that visited does not report long tasks (Safari, for one): profile the page from a Chromium browser to measure it" } : {},
|
|
1951
|
+
...no_shifts ? { cls: "the browser that visited does not report layout shifts (Safari, for one): profile the page from a Chromium browser to measure it" } : {}
|
|
1952
|
+
};
|
|
1953
|
+
const inputs = {
|
|
1954
|
+
assets,
|
|
1955
|
+
...extras.assets_missing ? { assets_missing: extras.assets_missing } : {},
|
|
1956
|
+
htmlBytes: extras.strip?.total ?? pa?.html.bytes ?? null,
|
|
1957
|
+
...(() => {
|
|
1958
|
+
const img = (visit?.images_oversized ?? []).reduce((s, i) => s + i.bytes * Math.max(0, 1 - i.shown[0] * i.shown[1] * i.dpr * i.dpr / (i.natural[0] * i.natural[1])), 0);
|
|
1959
|
+
const pre = (visit?.preloads_unused ?? []).reduce((s, p) => s + p.bytes, 0);
|
|
1960
|
+
return img + pre >= 10240 ? { wasted: {
|
|
1961
|
+
bytes: Math.round(img + pre),
|
|
1962
|
+
images: Math.round(img),
|
|
1963
|
+
preloads: pre
|
|
1964
|
+
} } : {};
|
|
1965
|
+
})(),
|
|
1966
|
+
hasIslands: islands.length > 0,
|
|
1967
|
+
browserSeen: client.length > 0 || !!visit,
|
|
1968
|
+
recovered: client.reduce((s, c) => s + c.recovered, 0),
|
|
1969
|
+
neverWoke: client.length ? islands.filter((r) => (r.wake === "load" || r.wake === "idle" || r.wake === "visible") && !seen_entry.has(r.entry)).length : 0,
|
|
1970
|
+
changed: visit?.islands?.filter((i) => i.changed && !i.recovered).length ?? 0,
|
|
1971
|
+
dataBytes: data_bytes,
|
|
1972
|
+
dataDetail: data_parts.filter(([, b]) => b > 0).map(([k, b]) => `${k} ${fmt_bytes(b)}`),
|
|
1973
|
+
serverMs: server_ms,
|
|
1974
|
+
vitals: v ? {
|
|
1975
|
+
lcp: v.lcp,
|
|
1976
|
+
cls: v.cls,
|
|
1977
|
+
inp: v.inp,
|
|
1978
|
+
fcp: v.fcp,
|
|
1979
|
+
ttfb: v.ttfb
|
|
1980
|
+
} : visit ? {
|
|
1981
|
+
lcp: visit.paints?.lcp ?? null,
|
|
1982
|
+
fcp: visit.paints?.fcp ?? null,
|
|
1983
|
+
ttfb: visit.nav?.start_hidden ? null : visit.nav?.res_start ?? null,
|
|
1984
|
+
cls: no_shifts ? null : visit.shifts?.length ? Math.round(visit.shifts.reduce((s, x) => s + x.value, 0) * 1e3) / 1e3 : 0,
|
|
1985
|
+
inp: null
|
|
1986
|
+
} : null,
|
|
1987
|
+
tbt,
|
|
1988
|
+
unmeasured
|
|
1989
|
+
};
|
|
1990
|
+
return page_score(inputs);
|
|
1991
|
+
}
|
|
1992
|
+
function report_json(a, meta, base, extras) {
|
|
1993
|
+
const dur = a.duration_ms || 1;
|
|
1994
|
+
const busy = a.busy_ms || 1;
|
|
1995
|
+
const net = extras.net.filter((c) => c.ms >= 0);
|
|
1996
|
+
const net_total = round1(net.reduce((s, c) => s + c.ms + (c.body_ms ?? 0), 0));
|
|
1997
|
+
const budget = a.buckets.filter((b) => b.self_ms > 0 && b.category !== "idle").map((b) => ({
|
|
1998
|
+
label: b.key,
|
|
1999
|
+
category: b.category,
|
|
2000
|
+
ms: b.self_ms,
|
|
2001
|
+
pct: round1(b.self_ms / dur * 100)
|
|
2002
|
+
}));
|
|
2003
|
+
if (a.idle_ms > 0) budget.push({
|
|
2004
|
+
label: "idle / waiting",
|
|
2005
|
+
category: "idle",
|
|
2006
|
+
ms: a.idle_ms,
|
|
2007
|
+
pct: round1(a.idle_ms / dur * 100)
|
|
2008
|
+
});
|
|
2009
|
+
budget.sort((x, y) => y.ms - x.ms);
|
|
2010
|
+
const alloc_by_name = /* @__PURE__ */ new Map();
|
|
2011
|
+
for (const h of extras.heap ?? []) alloc_by_name.set(h.name, (alloc_by_name.get(h.name) ?? 0) + h.self_bytes);
|
|
2012
|
+
for (const c of extras.heap_components ?? []) alloc_by_name.set(c.name, c.bytes);
|
|
2013
|
+
const verdict = a.idle_ms > dur * .5 ? "waiting" : a.busy_ms / dur * 100 > 60 ? "compute-bound" : "mixed";
|
|
2014
|
+
const hosts = top_hosts(net);
|
|
2015
|
+
return {
|
|
2016
|
+
schema: "ogygia-profiler-report",
|
|
2017
|
+
version: 1,
|
|
2018
|
+
engine: { version: TESTING_ENGINE_VERSION },
|
|
2019
|
+
units: {
|
|
2020
|
+
time: "ms",
|
|
2021
|
+
size: "bytes",
|
|
2022
|
+
memory_suffix_mb: true
|
|
2023
|
+
},
|
|
2024
|
+
id: meta.id,
|
|
2025
|
+
created: meta.created,
|
|
2026
|
+
kind: meta.trigger,
|
|
2027
|
+
node: meta.node,
|
|
2028
|
+
dev: !!meta.dev,
|
|
2029
|
+
sourcemapped: a.sourcemapped,
|
|
2030
|
+
...a.other_requests_ms ? { other_requests_ms: a.other_requests_ms } : {},
|
|
2031
|
+
target: {
|
|
2032
|
+
page: meta.page ?? null,
|
|
2033
|
+
redirected_from: meta.redirected_from ?? null,
|
|
2034
|
+
...meta.redirect_stub ? { redirect_stub: true } : {},
|
|
2035
|
+
runs: meta.runs ?? null,
|
|
2036
|
+
warmup_ms: meta.warmup_ms ?? null,
|
|
2037
|
+
run_status: meta.run_status ?? null,
|
|
2038
|
+
run_bytes: meta.run_bytes ?? null,
|
|
2039
|
+
cache_control: meta.run_cache_control ?? null,
|
|
2040
|
+
prerendered: meta.prerendered ? meta.run_static ? "file" : "route" : null,
|
|
2041
|
+
budget_note: meta.budget_note ?? null,
|
|
2042
|
+
runs_measured: meta.runs_measured ?? null,
|
|
2043
|
+
instance: meta.instance ?? null,
|
|
2044
|
+
lambda: meta.lambda ?? false,
|
|
2045
|
+
request: meta.request ?? null
|
|
2046
|
+
},
|
|
2047
|
+
summary: {
|
|
2048
|
+
window_ms: meta.duration_ms,
|
|
2049
|
+
busy_ms: a.busy_ms,
|
|
2050
|
+
overhead: meta.overhead ? {
|
|
2051
|
+
...meta.overhead,
|
|
2052
|
+
top: a.overhead_functions ?? []
|
|
2053
|
+
} : null,
|
|
2054
|
+
busy_pct: round1(a.busy_ms / dur * 100),
|
|
2055
|
+
idle_ms: a.idle_ms,
|
|
2056
|
+
gc_ms: extras.gc_attr ? extras.gc_attr.summary.total_ms : a.gc_ms,
|
|
2057
|
+
gc_sampled_ms: a.gc_ms,
|
|
2058
|
+
verdict,
|
|
2059
|
+
sample_count: a.sample_count,
|
|
2060
|
+
cpu_percent: meta.cpu_percent ?? null,
|
|
2061
|
+
elu_percent: meta.elu_percent ?? null,
|
|
2062
|
+
loop_delay_ms: meta.loop_delay ?? null,
|
|
2063
|
+
rss_mb: meta.rss_mb ?? null
|
|
2064
|
+
},
|
|
2065
|
+
score: page_score_of(meta, extras),
|
|
2066
|
+
findings: derive_findings(a, meta, extras),
|
|
2067
|
+
budget,
|
|
2068
|
+
timeline: a.timeline ? {
|
|
2069
|
+
window_ms: a.timeline.window_ms,
|
|
2070
|
+
cpu_ms: a.timeline.cpu_ms,
|
|
2071
|
+
wait_ms: a.timeline.wait_ms,
|
|
2072
|
+
unaccounted_ms: a.timeline.gap_ms,
|
|
2073
|
+
phases: a.timeline.phases.map((p) => ({
|
|
2074
|
+
phase: p.phase,
|
|
2075
|
+
label: PHASE_LABEL[p.phase],
|
|
2076
|
+
cpu_ms: p.cpu_ms,
|
|
2077
|
+
wait_ms: p.wait_ms
|
|
2078
|
+
})),
|
|
2079
|
+
steps: chain_steps(a.timeline).map((s) => ({
|
|
2080
|
+
at_ms: s.seg.t0,
|
|
2081
|
+
ms: s.ms,
|
|
2082
|
+
pct: round1(s.pct),
|
|
2083
|
+
kind: s.seg.kind,
|
|
2084
|
+
phase: s.seg.phase,
|
|
2085
|
+
what: s.seg.label,
|
|
2086
|
+
detail: s.seg.detail ?? null,
|
|
2087
|
+
file: s.seg.file || null,
|
|
2088
|
+
calls: s.seg.calls ?? null,
|
|
2089
|
+
within: s.seg.within ?? null
|
|
2090
|
+
})),
|
|
2091
|
+
parallelizable: a.timeline.parallelizable.map((g) => ({
|
|
2092
|
+
calls: g.calls,
|
|
2093
|
+
ms: g.ms,
|
|
2094
|
+
save_ms: g.save_ms
|
|
2095
|
+
})),
|
|
2096
|
+
lanes: (a.timeline.lanes ?? []).map((l) => ({
|
|
2097
|
+
file: l.file,
|
|
2098
|
+
level: l.level,
|
|
2099
|
+
kind: l.kind,
|
|
2100
|
+
t0_ms: l.t0,
|
|
2101
|
+
t1_ms: l.t1,
|
|
2102
|
+
cpu_ms: l.cpu_ms,
|
|
2103
|
+
wait_ms: l.wait_ms,
|
|
2104
|
+
awaited_parent: l.awaited_parent
|
|
2105
|
+
})),
|
|
2106
|
+
chain: a.timeline.chain ?? null,
|
|
2107
|
+
awaits: a.timeline.awaits?.edges ?? []
|
|
2108
|
+
} : null,
|
|
2109
|
+
spans: span_rows(extras.spans),
|
|
2110
|
+
paths: (a.paths ?? []).map((g) => ({
|
|
2111
|
+
owner: {
|
|
2112
|
+
name: g.owner.name,
|
|
2113
|
+
file: g.owner.url,
|
|
2114
|
+
line: g.owner.line,
|
|
2115
|
+
category: g.owner.category,
|
|
2116
|
+
total_ms: g.owner.total_ms,
|
|
2117
|
+
calls: g.owner.calls ?? null
|
|
2118
|
+
},
|
|
2119
|
+
ms: g.ms,
|
|
2120
|
+
pct_busy: round1(g.ms / busy * 100),
|
|
2121
|
+
share_of_owner: g.share,
|
|
2122
|
+
functions: g.fns.map((f) => ({
|
|
2123
|
+
name: f.name,
|
|
2124
|
+
file: f.url,
|
|
2125
|
+
line: f.line,
|
|
2126
|
+
category: f.category,
|
|
2127
|
+
package: f.pkg ?? null,
|
|
2128
|
+
ms: f.ms
|
|
2129
|
+
})),
|
|
2130
|
+
tree: g.tree
|
|
2131
|
+
})),
|
|
2132
|
+
ogygia: (() => {
|
|
2133
|
+
const og = [...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;
|
|
2134
|
+
if (!og) return null;
|
|
2135
|
+
const { island_rows, seed, hole_rows, ...totals } = og;
|
|
2136
|
+
const client = new Map((extras.client ?? []).map((c) => [c.entry, c]));
|
|
2137
|
+
const by_name = new Map(a.components.map((c) => [c.name, c]));
|
|
2138
|
+
let js_only = null;
|
|
2139
|
+
return {
|
|
2140
|
+
...totals,
|
|
2141
|
+
island_rows: group_islands(island_rows ?? []).map((r, _i, grouped) => {
|
|
2142
|
+
js_only ??= island_js_unique(grouped, extras.weights);
|
|
2143
|
+
const comp = by_name.get(island_name(r));
|
|
2144
|
+
const cl = client.get(r.entry);
|
|
2145
|
+
return {
|
|
2146
|
+
name: island_name(r),
|
|
2147
|
+
entry: r.entry,
|
|
2148
|
+
fp: r.fp,
|
|
2149
|
+
fingerprints: r.variants ?? 1,
|
|
2150
|
+
copies: r.count,
|
|
2151
|
+
wake: r.wake,
|
|
2152
|
+
ssr_ms: comp ? round1(comp.total_ms / runs_of(meta)) : null,
|
|
2153
|
+
props_bytes: r.props_bytes,
|
|
2154
|
+
canonical_bytes: r.canonical_bytes,
|
|
2155
|
+
json: r.json,
|
|
2156
|
+
devalue_culprit: r.culprit,
|
|
2157
|
+
seed_refs: r.refs,
|
|
2158
|
+
seed_ref_keys: r.ref_keys,
|
|
2159
|
+
js_bytes: island_js_bytes(r, extras.weights),
|
|
2160
|
+
js_only_bytes: js_only?.get(r.entry) ?? null,
|
|
2161
|
+
modules: [r.module_url, ...r.hints].filter(Boolean),
|
|
2162
|
+
interactivity: r.interactivity,
|
|
2163
|
+
...extras.hazards?.[r.entry]?.length ? { hazards: extras.hazards[r.entry].slice(0, 3) } : {},
|
|
2164
|
+
client: cl ? {
|
|
2165
|
+
hydrations: cl.n,
|
|
2166
|
+
p50_ms: cl.p50_ms,
|
|
2167
|
+
max_ms: cl.max_ms,
|
|
2168
|
+
load_p50_ms: cl.load_p50_ms,
|
|
2169
|
+
recovered: cl.recovered,
|
|
2170
|
+
...cl.reason ? { reason: cl.reason } : {}
|
|
2171
|
+
} : null
|
|
2172
|
+
};
|
|
2173
|
+
}),
|
|
2174
|
+
seed: seed ?? null,
|
|
2175
|
+
hole_rows: (hole_rows ?? []).map((h) => {
|
|
2176
|
+
const e = hole_economics(meta).get(h.id);
|
|
2177
|
+
return {
|
|
2178
|
+
id: h.id,
|
|
2179
|
+
component: h.name || null,
|
|
2180
|
+
props: h.props || null,
|
|
2181
|
+
when: h.when,
|
|
2182
|
+
hydrate: h.hydrate,
|
|
2183
|
+
max_age_s: h.ttl,
|
|
2184
|
+
copies: h.count,
|
|
2185
|
+
requests: e ? {
|
|
2186
|
+
hit: e.hit,
|
|
2187
|
+
miss: e.miss,
|
|
2188
|
+
uncached: e.none,
|
|
2189
|
+
avg_ms: round1(e.ms / Math.max(e.hit + e.miss + e.none, 1))
|
|
2190
|
+
} : null
|
|
2191
|
+
};
|
|
2192
|
+
})
|
|
2193
|
+
};
|
|
2194
|
+
})(),
|
|
2195
|
+
kit: {
|
|
2196
|
+
uneval_ms: round1(a.functions.filter((f) => f.pkg === "devalue" && f.name === "uneval").reduce((s, f) => s + f.self_ms, 0)),
|
|
2197
|
+
etag_ms: round1(a.functions.filter((f) => f.name === "hash" && f.pkg === "@sveltejs/kit").reduce((s, f) => s + f.self_ms, 0))
|
|
2198
|
+
},
|
|
2199
|
+
components: (() => {
|
|
2200
|
+
return [...a.components].sort((x, y) => y.self_ms - x.self_ms).map((c) => {
|
|
2201
|
+
const n = (c.calls ?? 0) || null;
|
|
2202
|
+
return {
|
|
2203
|
+
name: c.name,
|
|
2204
|
+
instances: n,
|
|
2205
|
+
file: c.url,
|
|
2206
|
+
path: c.path ?? null,
|
|
2207
|
+
line: c.line,
|
|
2208
|
+
column: c.col || null,
|
|
2209
|
+
self_ms: c.self_ms,
|
|
2210
|
+
total_ms: c.total_ms,
|
|
2211
|
+
per_call_ms: per_call(c.total_ms / runs_of(meta), n),
|
|
2212
|
+
pct_busy: round1(c.total_ms / busy * 100),
|
|
2213
|
+
alloc_bytes: alloc_by_name.get(c.name) ?? null,
|
|
2214
|
+
markup_ms: c.markup_ms ?? null,
|
|
2215
|
+
logic_ms: c.logic_ms ?? null,
|
|
2216
|
+
parent: c.parent ?? null,
|
|
2217
|
+
runs_ms: c.runs_ms ?? null,
|
|
2218
|
+
hot_lines: c.lines ?? null,
|
|
2219
|
+
source: c.src ?? null,
|
|
2220
|
+
callees: c.callees ?? null,
|
|
2221
|
+
stacks: (c.stacks ?? []).map((s) => ({
|
|
2222
|
+
ms: s.ms,
|
|
2223
|
+
frames: s.frames.map(frame_text)
|
|
2224
|
+
}))
|
|
2225
|
+
};
|
|
2226
|
+
});
|
|
2227
|
+
})(),
|
|
2228
|
+
hot_functions: (() => {
|
|
2229
|
+
return a.functions.slice(0, 80).map((f) => {
|
|
2230
|
+
const n = (f.calls ?? 0) || null;
|
|
2231
|
+
return {
|
|
2232
|
+
name: f.name,
|
|
2233
|
+
label: f.label ?? null,
|
|
2234
|
+
instances: n,
|
|
2235
|
+
file: f.url,
|
|
2236
|
+
path: f.path ?? null,
|
|
2237
|
+
line: f.line,
|
|
2238
|
+
column: f.col || null,
|
|
2239
|
+
category: f.category,
|
|
2240
|
+
package: f.pkg ?? null,
|
|
2241
|
+
self_ms: f.self_ms,
|
|
2242
|
+
total_ms: f.total_ms,
|
|
2243
|
+
per_call_ms: per_call(f.total_ms / runs_of(meta), n),
|
|
2244
|
+
stacks: (f.stacks ?? []).map((s) => ({
|
|
2245
|
+
ms: s.ms,
|
|
2246
|
+
frames: s.frames.map(frame_text)
|
|
2247
|
+
})),
|
|
2248
|
+
hot_lines: f.lines ?? null,
|
|
2249
|
+
source: f.src ?? null,
|
|
2250
|
+
callees: f.callees ?? null
|
|
2251
|
+
};
|
|
2252
|
+
});
|
|
2253
|
+
})(),
|
|
2254
|
+
files: a.files.filter((f) => f.category !== "idle").slice(0, 40).map((f) => ({
|
|
2255
|
+
file: f.key,
|
|
2256
|
+
category: f.category,
|
|
2257
|
+
self_ms: f.self_ms,
|
|
2258
|
+
pct_busy: round1(f.self_ms / busy * 100)
|
|
2259
|
+
})),
|
|
2260
|
+
network: {
|
|
2261
|
+
count: net.length,
|
|
2262
|
+
total_ms: net_total,
|
|
2263
|
+
sequential_ms: sequential_ms(extras.net),
|
|
2264
|
+
errors: extras.net.filter((c) => c.error).length,
|
|
2265
|
+
hosts: hosts.map((h) => ({
|
|
2266
|
+
host: h.host,
|
|
2267
|
+
calls: h.count,
|
|
2268
|
+
total_ms: h.total,
|
|
2269
|
+
p50_ms: h.p50,
|
|
2270
|
+
max_ms: h.max,
|
|
2271
|
+
errors: h.errors
|
|
2272
|
+
})),
|
|
2273
|
+
calls: [...net].sort((x, y) => y.ms + (y.body_ms ?? 0) - (x.ms + (x.body_ms ?? 0))).slice(0, 200).map((c) => ({
|
|
2274
|
+
method: c.method,
|
|
2275
|
+
url: c.url,
|
|
2276
|
+
host: c.host,
|
|
2277
|
+
status: c.status,
|
|
2278
|
+
wait_ms: c.ms,
|
|
2279
|
+
body_ms: c.body_ms ?? null,
|
|
2280
|
+
bytes: c.bytes ?? null,
|
|
2281
|
+
transfer_bytes: c.transfer_bytes ?? null,
|
|
2282
|
+
encoding: c.encoding ?? null,
|
|
2283
|
+
type: c.type ?? null,
|
|
2284
|
+
req_bytes: c.req_bytes ?? null,
|
|
2285
|
+
req_payload: c.req_payload ?? null,
|
|
2286
|
+
route: c.route ?? c.path ?? null,
|
|
2287
|
+
caller: c.caller ?? null,
|
|
2288
|
+
callers: c.callers ?? null,
|
|
2289
|
+
server_timing: c.timings ?? null,
|
|
2290
|
+
trace: c.trace ?? null,
|
|
2291
|
+
headers: c.headers ?? null,
|
|
2292
|
+
error: c.error ?? null
|
|
2293
|
+
}))
|
|
2294
|
+
},
|
|
2295
|
+
cold: meta.cold ? {
|
|
2296
|
+
ms: meta.cold.ms,
|
|
2297
|
+
busy_ms: meta.cold.busy_ms,
|
|
2298
|
+
files: cold_rows(a, meta),
|
|
2299
|
+
...meta.cold.calls !== void 0 ? { calls: meta.cold.calls } : {}
|
|
2300
|
+
} : null,
|
|
2301
|
+
browser: extras.vitals || extras.client_marks || extras.client_cpu || extras.visit ? {
|
|
2302
|
+
...extras.vitals,
|
|
2303
|
+
...extras.client_marks ? { marks: extras.client_marks } : {},
|
|
2304
|
+
...extras.visit ? { visit: {
|
|
2305
|
+
at: extras.visit.at,
|
|
2306
|
+
nav: extras.visit.nav,
|
|
2307
|
+
paints: extras.visit.paints,
|
|
2308
|
+
resources: extras.visit.resources_all ?? extras.visit.resources.length,
|
|
2309
|
+
resource_bytes: extras.visit.resource_totals ? extras.visit.resource_totals.reduce((s, t) => s + t.transfer, 0) : extras.visit.resources.reduce((s, r) => s + (r.transfer ?? 0), 0),
|
|
2310
|
+
...extras.visit.resource_totals ? { resource_totals: extras.visit.resource_totals } : {},
|
|
2311
|
+
longtasks: extras.visit.longtasks.length,
|
|
2312
|
+
islands: extras.visit.islands.map((i) => ({
|
|
2313
|
+
fp: i.fp,
|
|
2314
|
+
t0: i.t0,
|
|
2315
|
+
loaded: i.loaded,
|
|
2316
|
+
done: i.done,
|
|
2317
|
+
...i.changed ? { changed: true } : {}
|
|
2318
|
+
})),
|
|
2319
|
+
firsts: extras.visit.firsts,
|
|
2320
|
+
shifts: extras.visit.shifts.length,
|
|
2321
|
+
cls_by_island: Object.fromEntries(extras.visit.shifts.reduce((m, s) => m.set(s.fp ?? "(outside islands)", (m.get(s.fp ?? "(outside islands)") ?? 0) + s.value), /* @__PURE__ */ new Map()))
|
|
2322
|
+
} } : {},
|
|
2323
|
+
...extras.interaction_cpu ? { interaction_cpu: extras.interaction_cpu } : {},
|
|
2324
|
+
...extras.client_cpu ? { cpu: {
|
|
2325
|
+
at: extras.client_cpu.at,
|
|
2326
|
+
sampled_ms: extras.client_cpu.analysis.duration_ms,
|
|
2327
|
+
busy_ms: extras.client_cpu.analysis.busy_ms,
|
|
2328
|
+
...extras.client_cpu.windows ? {
|
|
2329
|
+
by_island: Object.entries(extras.client_cpu.windows.islands).map(([fp, w]) => ({
|
|
2330
|
+
fp,
|
|
2331
|
+
name: island_rows_of(meta).find((r) => r.fp === fp)?.name ?? null,
|
|
2332
|
+
ms: w.ms,
|
|
2333
|
+
top: w.top
|
|
2334
|
+
})),
|
|
2335
|
+
outside_islands: extras.client_cpu.windows.outside
|
|
2336
|
+
} : {},
|
|
2337
|
+
components: extras.client_cpu.analysis.components.slice(0, 30).map((c) => ({
|
|
2338
|
+
name: c.name,
|
|
2339
|
+
file: c.url,
|
|
2340
|
+
self_ms: c.self_ms,
|
|
2341
|
+
total_ms: c.total_ms,
|
|
2342
|
+
instances: c.calls ?? null
|
|
2343
|
+
})),
|
|
2344
|
+
hot_functions: extras.client_cpu.analysis.functions.slice(0, 30).map((f) => ({
|
|
2345
|
+
name: f.name,
|
|
2346
|
+
file: f.url,
|
|
2347
|
+
line: f.line,
|
|
2348
|
+
category: f.category,
|
|
2349
|
+
package: f.pkg ?? null,
|
|
2350
|
+
self_ms: f.self_ms,
|
|
2351
|
+
total_ms: f.total_ms
|
|
2352
|
+
}))
|
|
2353
|
+
} } : {}
|
|
2354
|
+
} : null,
|
|
2355
|
+
replay: meta.request ? {
|
|
2356
|
+
url: `${base}/replay/${meta.id}`,
|
|
2357
|
+
path: extras.replay?.path ?? meta.request.path,
|
|
2358
|
+
headers: Object.keys(extras.replay?.headers ?? {})
|
|
2359
|
+
} : null,
|
|
2360
|
+
assets: extras.assets ? {
|
|
2361
|
+
totals: extras.assets.totals,
|
|
2362
|
+
html: extras.assets.html,
|
|
2363
|
+
inline: extras.assets.inline,
|
|
2364
|
+
files: extras.assets.assets.slice(0, 40).map((x) => ({
|
|
2365
|
+
url: x.url,
|
|
2366
|
+
kind: x.kind,
|
|
2367
|
+
via: x.via,
|
|
2368
|
+
bytes: x.bytes,
|
|
2369
|
+
wire: x.wire,
|
|
2370
|
+
...x.blocking ? { blocking: true } : {},
|
|
2371
|
+
...x.lazy ? { lazy: true } : {},
|
|
2372
|
+
...x.contains ? { contains: x.contains } : {}
|
|
2373
|
+
})),
|
|
2374
|
+
...extras.assets.missed.length ? { missed: extras.assets.missed.slice(0, 20) } : {},
|
|
2375
|
+
...extras.assets.missed_why?.length ? { missed_why: extras.assets.missed_why } : {},
|
|
2376
|
+
...(() => {
|
|
2377
|
+
const twice = page_duplicates(extras).slice(0, 12);
|
|
2378
|
+
return twice.length ? { twice } : {};
|
|
2379
|
+
})()
|
|
2380
|
+
} : extras.assets_missing ? { missing: extras.assets_missing } : null,
|
|
2381
|
+
strip: extras.strip ? {
|
|
2382
|
+
total: extras.strip.total,
|
|
2383
|
+
by_kind: extras.strip.by_kind,
|
|
2384
|
+
shadow_count: extras.strip.shadow_count,
|
|
2385
|
+
segments: extras.strip.segments,
|
|
2386
|
+
...extras.strip.chunks ? { chunks: extras.strip.chunks } : {},
|
|
2387
|
+
...extras.strip.tail ? { tail: extras.strip.tail } : {}
|
|
2388
|
+
} : null,
|
|
2389
|
+
river: extras.river ?? null,
|
|
2390
|
+
memory: {
|
|
2391
|
+
rss_start_mb: extras.mem[0]?.rss ?? null,
|
|
2392
|
+
rss_end_mb: extras.mem.at(-1)?.rss ?? null,
|
|
2393
|
+
growth_mb: extras.mem.length >= 2 ? extras.mem.at(-1).rss - extras.mem[0].rss : 0,
|
|
2394
|
+
gc: extras.gc ?? null,
|
|
2395
|
+
gc_attribution: extras.gc_attr ? {
|
|
2396
|
+
...extras.gc_attr.summary,
|
|
2397
|
+
pauses: extras.gc_attr.pauses.map((p) => ({
|
|
2398
|
+
t_ms: p.t,
|
|
2399
|
+
ms: p.ms,
|
|
2400
|
+
ms_measured: p.ms_measured,
|
|
2401
|
+
kind: p.kind,
|
|
2402
|
+
forced: p.forced,
|
|
2403
|
+
since_ms: p.since_ms,
|
|
2404
|
+
allocated_bytes: p.allocated,
|
|
2405
|
+
estimated: p.estimated ?? false,
|
|
2406
|
+
why: p.why,
|
|
2407
|
+
running: p.running ?? null,
|
|
2408
|
+
top: p.top.map((x) => ({
|
|
2409
|
+
name: x.name,
|
|
2410
|
+
component: x.component,
|
|
2411
|
+
bytes: x.bytes,
|
|
2412
|
+
share: x.share
|
|
2413
|
+
}))
|
|
2414
|
+
})),
|
|
2415
|
+
makers: extras.gc_attr.makers.slice(0, 40).map((m) => ({
|
|
2416
|
+
name: m.name,
|
|
2417
|
+
file: m.url,
|
|
2418
|
+
line: m.line,
|
|
2419
|
+
category: m.category,
|
|
2420
|
+
component: m.component,
|
|
2421
|
+
via: m.caller ?? null,
|
|
2422
|
+
allocated_bytes: m.allocated,
|
|
2423
|
+
share: m.share,
|
|
2424
|
+
gc_ms: m.gc_ms,
|
|
2425
|
+
pauses: m.pauses
|
|
2426
|
+
})),
|
|
2427
|
+
components: extras.gc_attr.components.slice(0, 40)
|
|
2428
|
+
} : null,
|
|
2429
|
+
allocators: (extras.heap ?? []).map((h) => ({
|
|
2430
|
+
name: h.name,
|
|
2431
|
+
file: h.url,
|
|
2432
|
+
line: h.line,
|
|
2433
|
+
category: h.category,
|
|
2434
|
+
self_bytes: h.self_bytes,
|
|
2435
|
+
total_bytes: h.total_bytes
|
|
2436
|
+
})),
|
|
2437
|
+
by_component: (extras.heap_components ?? []).map((c) => ({
|
|
2438
|
+
name: c.name,
|
|
2439
|
+
bytes: c.bytes
|
|
2440
|
+
})),
|
|
2441
|
+
samples: extras.mem.map((m) => ({
|
|
2442
|
+
t_ms: m.t,
|
|
2443
|
+
rss_mb: m.rss,
|
|
2444
|
+
heap_used_mb: m.heap_used
|
|
2445
|
+
}))
|
|
2446
|
+
},
|
|
2447
|
+
deopts: a.deopts.map((d) => ({
|
|
2448
|
+
name: d.name,
|
|
2449
|
+
file: d.url,
|
|
2450
|
+
line: d.line,
|
|
2451
|
+
category: d.category,
|
|
2452
|
+
self_ms: d.self_ms,
|
|
2453
|
+
count: d.count,
|
|
2454
|
+
reasons: d.reasons
|
|
2455
|
+
})),
|
|
2456
|
+
sync_io: sync_io(a).map((s) => ({
|
|
2457
|
+
name: s.name,
|
|
2458
|
+
module: s.module,
|
|
2459
|
+
self_ms: s.self_ms,
|
|
2460
|
+
total_ms: s.total_ms,
|
|
2461
|
+
calls: s.calls,
|
|
2462
|
+
callers: s.callers
|
|
2463
|
+
})),
|
|
2464
|
+
memo_candidates: memo_candidates(a, extras.gc_attr?.makers ?? [], 8, runs_of(meta)),
|
|
2465
|
+
promises: extras.promises ? {
|
|
2466
|
+
count: extras.promises.count,
|
|
2467
|
+
per_render: meta.runs?.length ? Math.round(extras.promises.count / meta.runs.length) : extras.promises.count,
|
|
2468
|
+
top: extras.promises.top
|
|
2469
|
+
} : null,
|
|
2470
|
+
retained: extras.retained ? {
|
|
2471
|
+
total_bytes: extras.retained.total_bytes,
|
|
2472
|
+
render_ms: extras.retained.render_ms,
|
|
2473
|
+
sites: extras.retained.sites.map((s) => ({
|
|
2474
|
+
name: s.name,
|
|
2475
|
+
file: s.url,
|
|
2476
|
+
line: s.line,
|
|
2477
|
+
via: s.caller ?? null,
|
|
2478
|
+
component: s.component,
|
|
2479
|
+
bytes: s.bytes,
|
|
2480
|
+
share: s.share
|
|
2481
|
+
}))
|
|
2482
|
+
} : null,
|
|
2483
|
+
steps: a.timeline ? render_steps(a.timeline, a.stacks) ?? null : null,
|
|
2484
|
+
stacks: a.stacks ? {
|
|
2485
|
+
window_ms: a.stacks.window_ms,
|
|
2486
|
+
raw_samples: a.stacks.raw,
|
|
2487
|
+
frames: a.stacks.frames.map((f) => ({
|
|
2488
|
+
name: f.n,
|
|
2489
|
+
file: f.f ?? null,
|
|
2490
|
+
category: f.c,
|
|
2491
|
+
parent: f.p
|
|
2492
|
+
})),
|
|
2493
|
+
t_ms: a.stacks.t,
|
|
2494
|
+
d_ms: a.stacks.d,
|
|
2495
|
+
leaf: a.stacks.leaf
|
|
2496
|
+
} : null,
|
|
2497
|
+
alloc: extras.alloc ? {
|
|
2498
|
+
grown_mb: extras.alloc.grown_mb,
|
|
2499
|
+
period_ms: extras.alloc.period_ms,
|
|
2500
|
+
longest_gap_ms: extras.alloc.longest_gap_ms,
|
|
2501
|
+
window: extras.alloc.window ?? null,
|
|
2502
|
+
bursts: extras.alloc.bursts.map((b) => ({
|
|
2503
|
+
t0_ms: b.t0,
|
|
2504
|
+
t1_ms: b.t1,
|
|
2505
|
+
mb: b.mb,
|
|
2506
|
+
mb_per_s: b.rate,
|
|
2507
|
+
gc_inside: b.gc,
|
|
2508
|
+
running: b.running
|
|
2509
|
+
})),
|
|
2510
|
+
samples: extras.alloc.samples.map((s) => ({
|
|
2511
|
+
t_ms: s.t,
|
|
2512
|
+
heap_mb: s.mb
|
|
2513
|
+
}))
|
|
2514
|
+
} : null,
|
|
2515
|
+
contention: extras.contention ? {
|
|
2516
|
+
overlap_ms: extras.contention.overlap_ms,
|
|
2517
|
+
cpu_max_ms: extras.contention.cpu_max_ms,
|
|
2518
|
+
busy_share: extras.contention.busy_share,
|
|
2519
|
+
inflight_at_start: extras.contention.inflight_at_start,
|
|
2520
|
+
per_window: extras.contention.per_window,
|
|
2521
|
+
requests: extras.contention.requests,
|
|
2522
|
+
note: "cpu_max_ms is an upper bound: a request’s CPU is a process-wide delta over its lifetime, so overlapping requests carry some of each other’s"
|
|
2523
|
+
} : null,
|
|
2524
|
+
lineage: extras.lineage ? {
|
|
2525
|
+
keys: extras.lineage.keys.map((k) => ({
|
|
2526
|
+
key: k.key,
|
|
2527
|
+
from: k.from,
|
|
2528
|
+
shipped_bytes: k.shipped_bytes,
|
|
2529
|
+
load_wait_ms: k.load_wait_ms,
|
|
2530
|
+
verdict: k.verdict,
|
|
2531
|
+
readers: k.readers
|
|
2532
|
+
})),
|
|
2533
|
+
components: extras.lineage.components,
|
|
2534
|
+
unread: extras.lineage.unread.map((k) => k.key),
|
|
2535
|
+
server_only: extras.lineage.server_only.map((k) => k.key),
|
|
2536
|
+
notes: extras.lineage.notes
|
|
2537
|
+
} : null,
|
|
2538
|
+
ledger: extras.ledger ?? null,
|
|
2539
|
+
patterns: extras.patterns ? extras.patterns.map((p) => {
|
|
2540
|
+
const r = p.wait ? 1 : runs_of(meta);
|
|
2541
|
+
const r2 = (x) => Math.round(x * 100) / 100;
|
|
2542
|
+
return {
|
|
2543
|
+
...p,
|
|
2544
|
+
save_per_render_ms: r2(p.save_ms / r),
|
|
2545
|
+
cost_per_render_ms: r2(p.cost_ms / r)
|
|
2546
|
+
};
|
|
2547
|
+
}) : null,
|
|
2548
|
+
growth: extras.growth ?? null,
|
|
2549
|
+
drill: extras.drill ?? null,
|
|
2550
|
+
forecast: extras.forecast ?? null,
|
|
2551
|
+
span_values: span_values(extras.spans),
|
|
2552
|
+
waiting: (() => {
|
|
2553
|
+
const m = /* @__PURE__ */ new Map();
|
|
2554
|
+
const add = (caller, kind, ms) => {
|
|
2555
|
+
const k = caller + "|" + kind;
|
|
2556
|
+
const r = m.get(k) ?? {
|
|
2557
|
+
caller,
|
|
2558
|
+
kind,
|
|
2559
|
+
count: 0,
|
|
2560
|
+
wait_ms: 0
|
|
2561
|
+
};
|
|
2562
|
+
r.count++;
|
|
2563
|
+
r.wait_ms = round1(r.wait_ms + ms);
|
|
2564
|
+
m.set(k, r);
|
|
2565
|
+
};
|
|
2566
|
+
for (const c of net) if (c.caller) add(c.caller, "http", c.ms + (c.body_ms ?? 0));
|
|
2567
|
+
for (const o of extras.io ?? []) if (o.caller && !o.open) add(o.caller, io_kind(o.type), o.ms);
|
|
2568
|
+
return [...m.values()].sort((x, y) => y.wait_ms - x.wait_ms).slice(0, 40);
|
|
2569
|
+
})(),
|
|
2570
|
+
user_timings: (extras.measures ?? []).map((m) => ({
|
|
2571
|
+
name: m.name,
|
|
2572
|
+
count: m.count,
|
|
2573
|
+
total_ms: m.total_ms,
|
|
2574
|
+
avg_ms: round1(m.total_ms / m.count),
|
|
2575
|
+
max_ms: m.max_ms
|
|
2576
|
+
})),
|
|
2577
|
+
requests: meta.requests.map((r) => ({
|
|
2578
|
+
method: r.method,
|
|
2579
|
+
path: r.path,
|
|
2580
|
+
route: r.route,
|
|
2581
|
+
status: r.status,
|
|
2582
|
+
ms: r.ms,
|
|
2583
|
+
cpu_ms: r.cpu_ms,
|
|
2584
|
+
wait_ms: round1(Math.max(0, r.ms - r.cpu_ms)),
|
|
2585
|
+
net_ms: r.net_ms,
|
|
2586
|
+
net_count: r.net_count,
|
|
2587
|
+
inflight: r.inflight,
|
|
2588
|
+
internal: !!r.internal,
|
|
2589
|
+
tags: r.tags ?? null,
|
|
2590
|
+
span_count: r.span_count ?? null,
|
|
2591
|
+
span_ms: r.span_ms ?? null,
|
|
2592
|
+
hole: r.hole ?? null
|
|
2593
|
+
})),
|
|
2594
|
+
links: {
|
|
2595
|
+
html: `${base}/report/${meta.id}`,
|
|
2596
|
+
json: `${base}/report/${meta.id}.json`,
|
|
2597
|
+
cpuprofile: `${base}/report/${meta.id}/raw`
|
|
2598
|
+
}
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
function report_dump(a, meta, extras) {
|
|
2602
|
+
return {
|
|
2603
|
+
kind: "ogygia-profiler-dump",
|
|
2604
|
+
version: 1,
|
|
2605
|
+
meta,
|
|
2606
|
+
analysis: a,
|
|
2607
|
+
extras
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
/** Narrowing guard for an uploaded dump before we render it. */
|
|
2611
|
+
function is_dump(x) {
|
|
2612
|
+
const d = x;
|
|
2613
|
+
return !!d && typeof d === "object" && d.kind === "ogygia-profiler-dump" && !!d.meta && !!d.analysis && !!d.extras;
|
|
2614
|
+
}
|
|
2615
|
+
function top_hosts(net) {
|
|
2616
|
+
const by_host = /* @__PURE__ */ new Map();
|
|
2617
|
+
const errors = /* @__PURE__ */ new Map();
|
|
2618
|
+
for (const c of net) {
|
|
2619
|
+
const t = c.ms + (c.body_ms ?? 0);
|
|
2620
|
+
let list = by_host.get(c.host);
|
|
2621
|
+
if (!list) by_host.set(c.host, list = []);
|
|
2622
|
+
list.push(t);
|
|
2623
|
+
if (c.error) errors.set(c.host, (errors.get(c.host) ?? 0) + 1);
|
|
2624
|
+
}
|
|
2625
|
+
return [...by_host.entries()].map(([host, list]) => {
|
|
2626
|
+
const sorted = [...list].sort((a, b) => a - b);
|
|
2627
|
+
return {
|
|
2628
|
+
host,
|
|
2629
|
+
count: list.length,
|
|
2630
|
+
total: Math.round(list.reduce((a, c) => a + c, 0) * 100) / 100,
|
|
2631
|
+
p50: sorted[Math.floor(sorted.length / 2)] ?? 0,
|
|
2632
|
+
max: sorted.at(-1) ?? 0,
|
|
2633
|
+
errors: errors.get(host) ?? 0
|
|
2634
|
+
};
|
|
2635
|
+
}).sort((a, b) => b.total - a.total);
|
|
2636
|
+
}
|
|
2637
|
+
/** timeline of network calls, offset from window start */
|
|
2638
|
+
export { browser_of, cold_rows, derive_findings, dev_tool_caller, fair_shares, group_islands, hazard_fix, hazard_words, hole_economics, hole_label, hot_line_of, inline_threshold_tune, is_dump, island_host_renamer, island_js_bytes, island_js_unique, island_name, island_rows_of, kit_load_serialize_ms, lake_saving, link_patterns, mostly_waited, n_plus_one, own_requests, page_duplicates, page_origin, page_score_of, path_template, remote_function_name, report_dump, report_json, run_spread, run_trend, span_rows, start_js, with_hidden_filled };
|