@ogygia/devtools 0.0.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +24 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.js +24 -0
- package/dist/devtools/BoundaryOverlay.svelte +396 -0
- package/dist/devtools/BoundaryOverlay.svelte.d.ts +19 -0
- package/dist/devtools/Devtools.svelte +663 -0
- package/dist/devtools/Devtools.svelte.d.ts +15 -0
- package/dist/devtools/HubTab.svelte +206 -0
- package/dist/devtools/HubTab.svelte.d.ts +11 -0
- package/dist/devtools/HydrationTab.svelte +237 -0
- package/dist/devtools/HydrationTab.svelte.d.ts +13 -0
- package/dist/devtools/IslandDetail.svelte +615 -0
- package/dist/devtools/IslandDetail.svelte.d.ts +15 -0
- package/dist/devtools/LedgerTab.svelte +287 -0
- package/dist/devtools/LedgerTab.svelte.d.ts +11 -0
- package/dist/devtools/LensTab.svelte +290 -0
- package/dist/devtools/LensTab.svelte.d.ts +19 -0
- package/dist/devtools/NavTab.svelte +229 -0
- package/dist/devtools/NavTab.svelte.d.ts +11 -0
- package/dist/devtools/PageTab.svelte +1122 -0
- package/dist/devtools/PageTab.svelte.d.ts +15 -0
- package/dist/devtools/ProfilerTab.svelte +755 -0
- package/dist/devtools/ProfilerTab.svelte.d.ts +15 -0
- package/dist/devtools/PropsTree.svelte +155 -0
- package/dist/devtools/PropsTree.svelte.d.ts +15 -0
- package/dist/devtools/SessionTab.svelte +796 -0
- package/dist/devtools/SessionTab.svelte.d.ts +11 -0
- package/dist/devtools/TimelineTab.svelte +267 -0
- package/dist/devtools/TimelineTab.svelte.d.ts +11 -0
- package/dist/devtools/WireTab.svelte +149 -0
- package/dist/devtools/WireTab.svelte.d.ts +11 -0
- package/dist/devtools/bytes.d.ts +70 -0
- package/dist/devtools/bytes.js +82 -0
- package/dist/devtools/cache-headers.d.ts +20 -0
- package/dist/devtools/cache-headers.js +81 -0
- package/dist/devtools/highlight.d.ts +7 -0
- package/dist/devtools/highlight.js +83 -0
- package/dist/devtools/hydration.d.ts +48 -0
- package/dist/devtools/hydration.js +93 -0
- package/dist/devtools/index.d.ts +3 -0
- package/dist/devtools/index.js +3 -0
- package/dist/devtools/ledger-dom.d.ts +7 -0
- package/dist/devtools/ledger-dom.js +151 -0
- package/dist/devtools/leftovers.d.ts +77 -0
- package/dist/devtools/leftovers.js +319 -0
- package/dist/devtools/names.d.ts +1 -0
- package/dist/devtools/names.js +34 -0
- package/dist/devtools/page.d.ts +96 -0
- package/dist/devtools/page.js +957 -0
- package/dist/devtools/profile-anchors.d.ts +22 -0
- package/dist/devtools/profile-anchors.js +135 -0
- package/dist/devtools/profile-store.d.ts +175 -0
- package/dist/devtools/profile-store.js +239 -0
- package/dist/devtools/regions.d.ts +77 -0
- package/dist/devtools/regions.js +132 -0
- package/dist/devtools/session.d.ts +31 -0
- package/dist/devtools/session.js +455 -0
- package/dist/devtools/since-load.d.ts +50 -0
- package/dist/devtools/since-load.js +89 -0
- package/dist/devtools/source-lines.d.ts +24 -0
- package/dist/devtools/source-lines.js +106 -0
- package/dist/devtools/styles.d.ts +52 -0
- package/dist/devtools/styles.js +248 -0
- package/dist/devtools/testing-engine.d.ts +83 -0
- package/dist/devtools/testing-engine.js +220 -0
- package/dist/devtools/ui-app.d.ts +5 -0
- package/dist/devtools/ui-app.js +69 -0
- package/dist/devtools/ui.d.ts +50 -0
- package/dist/devtools/ui.js +175 -0
- package/dist/profiler/alloc.d.ts +75 -0
- package/dist/profiler/alloc.js +76 -0
- package/dist/profiler/analyze.d.ts +437 -0
- package/dist/profiler/analyze.js +1941 -0
- package/dist/profiler/app-path.d.ts +7 -0
- package/dist/profiler/app-path.js +26 -0
- package/dist/profiler/async-io.d.ts +58 -0
- package/dist/profiler/async-io.js +187 -0
- package/dist/profiler/beacon-standalone.d.ts +4 -0
- package/dist/profiler/beacon-standalone.js +409 -0
- package/dist/profiler/browser-findings.d.ts +25 -0
- package/dist/profiler/browser-findings.js +260 -0
- package/dist/profiler/byte-strip.d.ts +58 -0
- package/dist/profiler/byte-strip.js +228 -0
- package/dist/profiler/client-files.d.ts +16 -0
- package/dist/profiler/client-files.js +60 -0
- package/dist/profiler/client-windows.d.ts +49 -0
- package/dist/profiler/client-windows.js +95 -0
- package/dist/profiler/client.d.ts +40 -0
- package/dist/profiler/client.js +82 -0
- package/dist/profiler/compare.d.ts +308 -0
- package/dist/profiler/compare.js +733 -0
- package/dist/profiler/compress.d.ts +10 -0
- package/dist/profiler/compress.js +36 -0
- package/dist/profiler/contention.d.ts +60 -0
- package/dist/profiler/contention.js +86 -0
- package/dist/profiler/crypto.d.ts +19 -0
- package/dist/profiler/crypto.js +101 -0
- package/dist/profiler/doc-diff.d.ts +47 -0
- package/dist/profiler/doc-diff.js +214 -0
- package/dist/profiler/dom-diff.d.ts +43 -0
- package/dist/profiler/dom-diff.js +121 -0
- package/dist/profiler/drill.d.ts +89 -0
- package/dist/profiler/drill.js +553 -0
- package/dist/profiler/embedded-maps.d.ts +37 -0
- package/dist/profiler/embedded-maps.js +135 -0
- package/dist/profiler/env.d.ts +6 -0
- package/dist/profiler/env.js +10 -0
- package/dist/profiler/forecast.d.ts +76 -0
- package/dist/profiler/forecast.js +225 -0
- package/dist/profiler/frames.d.ts +11 -0
- package/dist/profiler/frames.js +42 -0
- package/dist/profiler/gc.d.ts +144 -0
- package/dist/profiler/gc.js +271 -0
- package/dist/profiler/hole-slots.d.ts +31 -0
- package/dist/profiler/hole-slots.js +90 -0
- package/dist/profiler/index.d.ts +288 -0
- package/dist/profiler/index.js +4921 -0
- package/dist/profiler/insights.d.ts +96 -0
- package/dist/profiler/insights.js +239 -0
- package/dist/profiler/ledger.d.ts +121 -0
- package/dist/profiler/ledger.js +440 -0
- package/dist/profiler/lineage.d.ts +81 -0
- package/dist/profiler/lineage.js +359 -0
- package/dist/profiler/module-map.d.ts +12 -0
- package/dist/profiler/module-map.js +94 -0
- package/dist/profiler/net.d.ts +169 -0
- package/dist/profiler/net.js +586 -0
- package/dist/profiler/on-demand.d.ts +11 -0
- package/dist/profiler/on-demand.js +15 -0
- package/dist/profiler/own-requests.d.ts +9 -0
- package/dist/profiler/own-requests.js +17 -0
- package/dist/profiler/patterns.d.ts +476 -0
- package/dist/profiler/patterns.js +2203 -0
- package/dist/profiler/profiler-router.d.ts +245 -0
- package/dist/profiler/profiler-router.js +113 -0
- package/dist/profiler/refresh-stub.d.ts +2 -0
- package/dist/profiler/refresh-stub.js +37 -0
- package/dist/profiler/report.d.ts +1404 -0
- package/dist/profiler/report.js +2638 -0
- package/dist/profiler/river.d.ts +65 -0
- package/dist/profiler/river.js +298 -0
- package/dist/profiler/route-files.d.ts +22 -0
- package/dist/profiler/route-files.js +55 -0
- package/dist/profiler/route-imports.d.ts +14 -0
- package/dist/profiler/route-imports.js +83 -0
- package/dist/profiler/saving.d.ts +22 -0
- package/dist/profiler/saving.js +42 -0
- package/dist/profiler/score.d.ts +109 -0
- package/dist/profiler/score.js +261 -0
- package/dist/profiler/session-cookie.d.ts +28 -0
- package/dist/profiler/session-cookie.js +45 -0
- package/dist/profiler/sink.d.ts +81 -0
- package/dist/profiler/sink.js +124 -0
- package/dist/profiler/site-fixes.d.ts +25 -0
- package/dist/profiler/site-fixes.js +62 -0
- package/dist/profiler/source-scan.d.ts +210 -0
- package/dist/profiler/source-scan.js +1724 -0
- package/dist/profiler/span.d.ts +90 -0
- package/dist/profiler/span.js +109 -0
- package/dist/profiler/stacks.d.ts +43 -0
- package/dist/profiler/stacks.js +107 -0
- package/dist/profiler/standalone.d.ts +27 -0
- package/dist/profiler/standalone.js +98 -0
- package/dist/profiler/steps.d.ts +39 -0
- package/dist/profiler/steps.js +74 -0
- package/dist/profiler/storage/index.d.ts +87 -0
- package/dist/profiler/storage/index.js +76 -0
- package/dist/profiler/storage/postgres.d.ts +12 -0
- package/dist/profiler/storage/postgres.js +94 -0
- package/dist/profiler/storage/redis.d.ts +19 -0
- package/dist/profiler/storage/redis.js +148 -0
- package/dist/profiler/storage/sqlite.d.ts +2 -0
- package/dist/profiler/storage/sqlite.js +85 -0
- package/dist/profiler/storage/visits.d.ts +4 -0
- package/dist/profiler/storage/visits.js +6 -0
- package/dist/profiler/stream-tail.d.ts +26 -0
- package/dist/profiler/stream-tail.js +74 -0
- package/dist/profiler/timeline.d.ts +279 -0
- package/dist/profiler/timeline.js +690 -0
- package/dist/profiler/ui/AllocStrip.svelte +146 -0
- package/dist/profiler/ui/AllocStrip.svelte.d.ts +17 -0
- package/dist/profiler/ui/Awaits.svelte +363 -0
- package/dist/profiler/ui/Awaits.svelte.d.ts +16 -0
- package/dist/profiler/ui/ByteStrip.svelte +182 -0
- package/dist/profiler/ui/ByteStrip.svelte.d.ts +18 -0
- package/dist/profiler/ui/Compare.svelte +24 -0
- package/dist/profiler/ui/Compare.svelte.d.ts +16 -0
- package/dist/profiler/ui/CompareBody.svelte +429 -0
- package/dist/profiler/ui/CompareBody.svelte.d.ts +8 -0
- package/dist/profiler/ui/ComponentsTable.svelte +265 -0
- package/dist/profiler/ui/ComponentsTable.svelte.d.ts +20 -0
- package/dist/profiler/ui/Dashboard.svelte +492 -0
- package/dist/profiler/ui/Dashboard.svelte.d.ts +31 -0
- package/dist/profiler/ui/DataRiver.svelte +206 -0
- package/dist/profiler/ui/DataRiver.svelte.d.ts +13 -0
- package/dist/profiler/ui/DomTravel.svelte +207 -0
- package/dist/profiler/ui/DomTravel.svelte.d.ts +9 -0
- package/dist/profiler/ui/DownloadHtml.svelte +52 -0
- package/dist/profiler/ui/DownloadHtml.svelte.d.ts +10 -0
- package/dist/profiler/ui/Drill.svelte +276 -0
- package/dist/profiler/ui/Drill.svelte.d.ts +12 -0
- package/dist/profiler/ui/ExportButton.svelte +59 -0
- package/dist/profiler/ui/ExportButton.svelte.d.ts +8 -0
- package/dist/profiler/ui/Flame.svelte +222 -0
- package/dist/profiler/ui/Flame.svelte.d.ts +12 -0
- package/dist/profiler/ui/FrameDetails.svelte +308 -0
- package/dist/profiler/ui/FrameDetails.svelte.d.ts +16 -0
- package/dist/profiler/ui/FunctionsTable.svelte +156 -0
- package/dist/profiler/ui/FunctionsTable.svelte.d.ts +17 -0
- package/dist/profiler/ui/IslandHeatmap.svelte +151 -0
- package/dist/profiler/ui/IslandHeatmap.svelte.d.ts +16 -0
- package/dist/profiler/ui/IslandsTable.svelte +448 -0
- package/dist/profiler/ui/IslandsTable.svelte.d.ts +16 -0
- package/dist/profiler/ui/KeepLocal.svelte +46 -0
- package/dist/profiler/ui/KeepLocal.svelte.d.ts +8 -0
- package/dist/profiler/ui/Ledger.svelte +196 -0
- package/dist/profiler/ui/Ledger.svelte.d.ts +18 -0
- package/dist/profiler/ui/LocalCompare.svelte +48 -0
- package/dist/profiler/ui/LocalCompare.svelte.d.ts +8 -0
- package/dist/profiler/ui/LocalReports.svelte +68 -0
- package/dist/profiler/ui/LocalReports.svelte.d.ts +7 -0
- package/dist/profiler/ui/Login.svelte +13 -0
- package/dist/profiler/ui/Login.svelte.d.ts +12 -0
- package/dist/profiler/ui/LoginForm.svelte +42 -0
- package/dist/profiler/ui/LoginForm.svelte.d.ts +7 -0
- package/dist/profiler/ui/Message.svelte +10 -0
- package/dist/profiler/ui/Message.svelte.d.ts +12 -0
- package/dist/profiler/ui/OnThisPage.svelte +137 -0
- package/dist/profiler/ui/OnThisPage.svelte.d.ts +3 -0
- package/dist/profiler/ui/OneClock.svelte +297 -0
- package/dist/profiler/ui/OneClock.svelte.d.ts +27 -0
- package/dist/profiler/ui/Panel.svelte +10 -0
- package/dist/profiler/ui/Panel.svelte.d.ts +16 -0
- package/dist/profiler/ui/Paths.svelte +269 -0
- package/dist/profiler/ui/Paths.svelte.d.ts +15 -0
- package/dist/profiler/ui/Patterns.svelte +364 -0
- package/dist/profiler/ui/Patterns.svelte.d.ts +15 -0
- package/dist/profiler/ui/PermalinkGate.svelte +143 -0
- package/dist/profiler/ui/PermalinkGate.svelte.d.ts +10 -0
- package/dist/profiler/ui/RenderSteps.svelte +218 -0
- package/dist/profiler/ui/RenderSteps.svelte.d.ts +13 -0
- package/dist/profiler/ui/Report.svelte +28 -0
- package/dist/profiler/ui/Report.svelte.d.ts +20 -0
- package/dist/profiler/ui/ReportBody.svelte +2592 -0
- package/dist/profiler/ui/ReportBody.svelte.d.ts +21 -0
- package/dist/profiler/ui/Run.svelte +17 -0
- package/dist/profiler/ui/Run.svelte.d.ts +14 -0
- package/dist/profiler/ui/RunView.svelte +146 -0
- package/dist/profiler/ui/RunView.svelte.d.ts +10 -0
- package/dist/profiler/ui/ScoreCard.svelte +197 -0
- package/dist/profiler/ui/ScoreCard.svelte.d.ts +7 -0
- package/dist/profiler/ui/Scrub.svelte +293 -0
- package/dist/profiler/ui/Scrub.svelte.d.ts +18 -0
- package/dist/profiler/ui/SeedExplainer.svelte +193 -0
- package/dist/profiler/ui/SeedExplainer.svelte.d.ts +16 -0
- package/dist/profiler/ui/ShareLink.svelte +148 -0
- package/dist/profiler/ui/ShareLink.svelte.d.ts +8 -0
- package/dist/profiler/ui/Shell.svelte +50 -0
- package/dist/profiler/ui/Shell.svelte.d.ts +27 -0
- package/dist/profiler/ui/Sidebar.svelte +312 -0
- package/dist/profiler/ui/Sidebar.svelte.d.ts +7 -0
- package/dist/profiler/ui/Site.svelte +20 -0
- package/dist/profiler/ui/Site.svelte.d.ts +23 -0
- package/dist/profiler/ui/SiteView.svelte +385 -0
- package/dist/profiler/ui/SiteView.svelte.d.ts +27 -0
- package/dist/profiler/ui/ThemeSwitch.svelte +78 -0
- package/dist/profiler/ui/ThemeSwitch.svelte.d.ts +3 -0
- package/dist/profiler/ui/Timeline.svelte +672 -0
- package/dist/profiler/ui/Timeline.svelte.d.ts +22 -0
- package/dist/profiler/ui/Treemap.svelte +278 -0
- package/dist/profiler/ui/Treemap.svelte.d.ts +20 -0
- package/dist/profiler/ui/Upload.svelte +19 -0
- package/dist/profiler/ui/Upload.svelte.d.ts +10 -0
- package/dist/profiler/ui/UploadForm.svelte +59 -0
- package/dist/profiler/ui/UploadForm.svelte.d.ts +6 -0
- package/dist/profiler/ui/WaitingTable.svelte +55 -0
- package/dist/profiler/ui/WaitingTable.svelte.d.ts +14 -0
- package/dist/profiler/ui/Waterfall.svelte +371 -0
- package/dist/profiler/ui/Waterfall.svelte.d.ts +7 -0
- package/dist/profiler/ui/WhatIf.svelte +87 -0
- package/dist/profiler/ui/WhatIf.svelte.d.ts +18 -0
- package/dist/profiler/ui/fake-progress.svelte.d.ts +15 -0
- package/dist/profiler/ui/fake-progress.svelte.js +38 -0
- package/dist/profiler/ui/format.d.ts +20 -0
- package/dist/profiler/ui/format.js +71 -0
- package/dist/profiler/ui/permalink.d.ts +9 -0
- package/dist/profiler/ui/permalink.js +108 -0
- package/dist/profiler/ui/press.d.ts +5 -0
- package/dist/profiler/ui/press.js +19 -0
- package/dist/profiler/ui/profiler.css +650 -0
- package/dist/profiler/ui/report-data.d.ts +189 -0
- package/dist/profiler/ui/report-data.js +393 -0
- package/dist/profiler/ui/report-request.d.ts +6 -0
- package/dist/profiler/ui/report-request.js +50 -0
- package/dist/profiler/ui/row-anchor.svelte.d.ts +12 -0
- package/dist/profiler/ui/row-anchor.svelte.js +44 -0
- package/dist/profiler/ui/sort.svelte.d.ts +14 -0
- package/dist/profiler/ui/sort.svelte.js +37 -0
- package/dist/profiler/ui/store.d.ts +45 -0
- package/dist/profiler/ui/store.js +92 -0
- package/dist/profiler/visit.d.ts +389 -0
- package/dist/profiler/visit.js +1002 -0
- package/dist/report.d.ts +5 -0
- package/dist/report.js +4 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +38 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.js +78 -0
- package/package.json +99 -6
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export type ScoreKey = "js" | "weight" | "blocking" | "html" | "data" | "hydration" | "server" | "loading" | "stability" | "responsiveness";
|
|
2
|
+
/** A category's contribution: its own 0–100 score, the weight it carries, and why. */
|
|
3
|
+
export interface ScoreCategory {
|
|
4
|
+
key: ScoreKey;
|
|
5
|
+
label: string;
|
|
6
|
+
/** 0–100 */
|
|
7
|
+
score: number;
|
|
8
|
+
/** relative weight BEFORE renormalization */
|
|
9
|
+
weight: number;
|
|
10
|
+
/** the measured value behind the score, already formatted */
|
|
11
|
+
value: string;
|
|
12
|
+
/** one line: what this measures and, when it lost points, why */
|
|
13
|
+
note: string;
|
|
14
|
+
/** the biggest contributors (files, islands, metrics), already formatted */
|
|
15
|
+
detail?: string[];
|
|
16
|
+
/** the points it cost the total (its weight share × the gap to 100), filled at the end */
|
|
17
|
+
lost?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PageScore {
|
|
20
|
+
/** 0–100, weighted over the categories that had data */
|
|
21
|
+
score: number;
|
|
22
|
+
/** A ≥90, B ≥75, C ≥60, D ≥40, else F */
|
|
23
|
+
grade: "A" | "B" | "C" | "D" | "F";
|
|
24
|
+
categories: ScoreCategory[];
|
|
25
|
+
/** the single biggest point loss — what to fix first, or null at a top score */
|
|
26
|
+
worst: ScoreCategory | null;
|
|
27
|
+
/** categories left out for want of a measurement, with why */
|
|
28
|
+
missing: {
|
|
29
|
+
key: ScoreKey;
|
|
30
|
+
label: string;
|
|
31
|
+
why: string;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
export interface ScoreAssets {
|
|
35
|
+
/** JS run at start (document scripts, modulepreloads, their static imports, at-start islands), decoded */
|
|
36
|
+
js: number;
|
|
37
|
+
js_wire: number;
|
|
38
|
+
js_files: number;
|
|
39
|
+
/** JS loaded later, on demand (lazy islands), decoded */
|
|
40
|
+
lazy_js: number;
|
|
41
|
+
css: number;
|
|
42
|
+
/** every byte at start on the wire, HTML included */
|
|
43
|
+
wire: number;
|
|
44
|
+
/** stylesheets + classic sync scripts in <head>, decoded */
|
|
45
|
+
blocking: number;
|
|
46
|
+
blocking_count: number;
|
|
47
|
+
/** the biggest files, formatted `name size` */
|
|
48
|
+
top?: string[];
|
|
49
|
+
}
|
|
50
|
+
export interface ScoreInputs {
|
|
51
|
+
/** the page's measured weight (page-assets.ts), or null when it could not be weighed (a dev
|
|
52
|
+
* server serves modules one by one: its graph says nothing about a build) */
|
|
53
|
+
assets: ScoreAssets | null;
|
|
54
|
+
/** why `assets` is null, for the report */
|
|
55
|
+
assets_missing?: string;
|
|
56
|
+
/** the HTML document, decoded bytes (null: not captured) */
|
|
57
|
+
htmlBytes: number | null;
|
|
58
|
+
/** what a browser visit fetched for nothing: pixels past what each image's box shows, preloads
|
|
59
|
+
* nothing used (said in the page weight's note; the score stays the weighed bytes') */
|
|
60
|
+
wasted?: {
|
|
61
|
+
bytes: number;
|
|
62
|
+
images: number;
|
|
63
|
+
preloads: number;
|
|
64
|
+
};
|
|
65
|
+
/** does the page have islands at all (hydration integrity is about islands) */
|
|
66
|
+
hasIslands: boolean;
|
|
67
|
+
/** hydrations that discarded the server DOM and re-rendered */
|
|
68
|
+
recovered: number;
|
|
69
|
+
/** islands that should have woken but never reported */
|
|
70
|
+
neverWoke: number;
|
|
71
|
+
/** islands whose markup changed on hydration without a recovery */
|
|
72
|
+
changed?: number;
|
|
73
|
+
/** has the browser reported at all (no visits: integrity unknown) */
|
|
74
|
+
browserSeen: boolean;
|
|
75
|
+
/** data shipped for hydration: the page seed + props + remote seed + inline script text, bytes */
|
|
76
|
+
dataBytes: number;
|
|
77
|
+
dataDetail?: string[];
|
|
78
|
+
/** SSR render time, ms (p50). null when not measured */
|
|
79
|
+
serverMs: number | null;
|
|
80
|
+
/** the browser's vitals over the visits, or null when none reported */
|
|
81
|
+
vitals: {
|
|
82
|
+
lcp: number | null;
|
|
83
|
+
cls: number | null;
|
|
84
|
+
inp: number | null;
|
|
85
|
+
fcp?: number | null;
|
|
86
|
+
ttfb?: number | null;
|
|
87
|
+
} | null;
|
|
88
|
+
/** total blocking time from the visits' long tasks (Σ over each task of ms − 50), or null */
|
|
89
|
+
tbt: number | null;
|
|
90
|
+
/** measures the visiting browser could not take, with why (`tbt`, `cls`): named in `missing`,
|
|
91
|
+
* never scored as a clean zero */
|
|
92
|
+
unmeasured?: {
|
|
93
|
+
tbt?: string;
|
|
94
|
+
cls?: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** 0–100 on a log-normal curve through (p10 → 90) and (median → 50). Lower values score higher. */
|
|
98
|
+
export declare function curve(value: number, p10: number, median: number): number;
|
|
99
|
+
export declare function fmt_bytes(bytes: number): string;
|
|
100
|
+
type CurveKey = "js" | "weight" | "blocking" | "html" | "data" | "server" | "lcp" | "fcp" | "ttfb" | "cls" | "inp" | "tbt";
|
|
101
|
+
/** The control points, in one place: [p10 (scores 90), median (scores 50)]. */
|
|
102
|
+
export declare const CURVES: { readonly [K in CurveKey]: readonly [p10: number, median: number]; };
|
|
103
|
+
/**
|
|
104
|
+
* Weights (before renormalization): JS 30 · server 15 · hydration 15 · loading 10 · data 10 ·
|
|
105
|
+
* weight 10 · responsiveness 10 · blocking 5 · HTML 5 · stability 5. What a visitor pays first —
|
|
106
|
+
* JS to run, time to first byte, a page that works as it wakes — carries the most.
|
|
107
|
+
*/
|
|
108
|
+
export declare function page_score(inp: ScoreInputs): PageScore;
|
|
109
|
+
export {};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// src/profiler/score.ts
|
|
2
|
+
/**
|
|
3
|
+
* THE PAGE SCORE — one 0–100 number for a page, scored on what a visitor pays for, whatever built
|
|
4
|
+
* the page: every byte of JS it runs at start (islands or not — a Kit-hydrated page's whole start
|
|
5
|
+
* graph counts), the weight on the wire, what blocks the first paint, the HTML, the data shipped for
|
|
6
|
+
* hydration, the server's render time, hydration integrity, and the browser's own vitals. A page
|
|
7
|
+
* built with ogygia wins by shipping less, not because the score only looks at islands.
|
|
8
|
+
*
|
|
9
|
+
* CURVES, NOT CLIFFS: each metric maps to 0–100 on a log-normal curve (the one Lighthouse uses): a
|
|
10
|
+
* control point where it scores 90 (`p10`) and one where it scores 50 (`median`). Twice the median
|
|
11
|
+
* still scores a little, half the p10 is near 100 — every byte and millisecond moves the number.
|
|
12
|
+
*
|
|
13
|
+
* Pure and inputs-only so it unit-tests trivially and the curves are one place to tune. A category
|
|
14
|
+
* with no measurement is DROPPED (never a free 100) and the weights renormalize — the report says
|
|
15
|
+
* which were left out and why.
|
|
16
|
+
*/
|
|
17
|
+
const KB = 1024;
|
|
18
|
+
const MB = 1048576;
|
|
19
|
+
/** erf, Abramowitz–Stegun 7.1.26 (|error| < 1.5e-7) — as Lighthouse's statistics module. */
|
|
20
|
+
function erf(x) {
|
|
21
|
+
const sign = Math.sign(x);
|
|
22
|
+
x = Math.abs(x);
|
|
23
|
+
const a1 = .254829592;
|
|
24
|
+
const a2 = -.284496736;
|
|
25
|
+
const a3 = 1.421413741;
|
|
26
|
+
const a4 = -1.453152027;
|
|
27
|
+
const a5 = 1.061405429;
|
|
28
|
+
const t = 1 / (1 + .3275911 * x);
|
|
29
|
+
return sign * (1 - t * (a1 + t * (a2 + t * (a3 + t * (a4 + t * a5)))) * Math.exp(-x * x));
|
|
30
|
+
}
|
|
31
|
+
/** 0–100 on a log-normal curve through (p10 → 90) and (median → 50). Lower values score higher. */
|
|
32
|
+
function curve(value, p10, median) {
|
|
33
|
+
if (!(value > 0)) return 100;
|
|
34
|
+
const INVERSE_ERFC_ONE_FIFTH = .9061938024368232;
|
|
35
|
+
const x = Math.log(value / median);
|
|
36
|
+
const p10_log = -Math.log(p10 / median);
|
|
37
|
+
const s = (1 - erf(x * INVERSE_ERFC_ONE_FIFTH / p10_log)) / 2;
|
|
38
|
+
const n = Math.round(s * 100);
|
|
39
|
+
return Math.max(0, Math.min(100, s >= .899999 ? n : Math.min(89, n)));
|
|
40
|
+
}
|
|
41
|
+
function grade_of(score) {
|
|
42
|
+
if (score >= 90) return "A";
|
|
43
|
+
if (score >= 75) return "B";
|
|
44
|
+
if (score >= 60) return "C";
|
|
45
|
+
if (score >= 40) return "D";
|
|
46
|
+
return "F";
|
|
47
|
+
}
|
|
48
|
+
function fmt_bytes(bytes) {
|
|
49
|
+
if (bytes >= MB) return `${(bytes / MB).toFixed(bytes >= 10 * MB ? 0 : 1)} MB`;
|
|
50
|
+
if (bytes >= KB) return `${Math.round(bytes / KB)} KB`;
|
|
51
|
+
return `${bytes} B`;
|
|
52
|
+
}
|
|
53
|
+
const ms = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)} s` : `${Math.round(n)} ms`;
|
|
54
|
+
/** The control points, in one place: [p10 (scores 90), median (scores 50)]. */
|
|
55
|
+
const CURVES = {
|
|
56
|
+
js: [150 * KB, 700 * KB],
|
|
57
|
+
weight: [400 * KB, 1.6 * MB],
|
|
58
|
+
blocking: [25 * KB, 150 * KB],
|
|
59
|
+
html: [80 * KB, 500 * KB],
|
|
60
|
+
data: [15 * KB, 150 * KB],
|
|
61
|
+
server: [50, 300],
|
|
62
|
+
lcp: [1200, 2500],
|
|
63
|
+
fcp: [900, 1800],
|
|
64
|
+
ttfb: [200, 800],
|
|
65
|
+
cls: [.05, .2],
|
|
66
|
+
inp: [100, 300],
|
|
67
|
+
tbt: [100, 400]
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Weights (before renormalization): JS 30 · server 15 · hydration 15 · loading 10 · data 10 ·
|
|
71
|
+
* weight 10 · responsiveness 10 · blocking 5 · HTML 5 · stability 5. What a visitor pays first —
|
|
72
|
+
* JS to run, time to first byte, a page that works as it wakes — carries the most.
|
|
73
|
+
*/
|
|
74
|
+
function page_score(inp) {
|
|
75
|
+
const cats = [];
|
|
76
|
+
const missing = [];
|
|
77
|
+
const a = inp.assets;
|
|
78
|
+
if (a) {
|
|
79
|
+
const [p10, med] = CURVES.js;
|
|
80
|
+
const js = curve(a.js, p10, med);
|
|
81
|
+
cats.push({
|
|
82
|
+
key: "js",
|
|
83
|
+
label: "JS at start",
|
|
84
|
+
score: js,
|
|
85
|
+
weight: 30,
|
|
86
|
+
value: `${fmt_bytes(a.js)} in ${a.js_files} file${a.js_files === 1 ? "" : "s"}`,
|
|
87
|
+
note: js >= 90 ? `The browser runs ${fmt_bytes(a.js)} of JS to start the page${a.lazy_js ? `; ${fmt_bytes(a.lazy_js)} more loads only when an island needs it` : ""}.` : `The browser downloads, parses and runs ${fmt_bytes(a.js)} of JS before the page is ready (${fmt_bytes(a.js_wire)} on the wire)${a.lazy_js ? `, plus ${fmt_bytes(a.lazy_js)} on demand` : ""}. Every script counts, not only islands: make static parts plain HTML (a lake), move heavy imports to the server, wake islands when visible or on interaction.`,
|
|
88
|
+
...a.top?.length ? { detail: a.top } : {}
|
|
89
|
+
});
|
|
90
|
+
const wt = curve(a.wire, CURVES.weight[0], CURVES.weight[1]);
|
|
91
|
+
cats.push({
|
|
92
|
+
key: "weight",
|
|
93
|
+
label: "Page weight",
|
|
94
|
+
score: wt,
|
|
95
|
+
weight: 10,
|
|
96
|
+
value: `${fmt_bytes(a.wire)} on the wire`,
|
|
97
|
+
note: (wt >= 90 ? "Light on the wire." : `${fmt_bytes(a.wire)} crosses the network before the page is complete (HTML, scripts, styles, fonts, images at start). Slow connections pay for every one.`) + (inp.wasted ? ` A visit fetched ${fmt_bytes(inp.wasted.bytes)} for nothing: ${[inp.wasted.images ? `${fmt_bytes(inp.wasted.images)} of image pixels bigger than their boxes` : "", inp.wasted.preloads ? `${fmt_bytes(inp.wasted.preloads)} of preloads nothing used` : ""].filter(Boolean).join(" and ")}.` : "")
|
|
98
|
+
});
|
|
99
|
+
const bl = curve(a.blocking + a.blocking_count * 8 * KB, CURVES.blocking[0], CURVES.blocking[1]);
|
|
100
|
+
cats.push({
|
|
101
|
+
key: "blocking",
|
|
102
|
+
label: "Render-blocking",
|
|
103
|
+
score: bl,
|
|
104
|
+
weight: 5,
|
|
105
|
+
value: `${a.blocking_count} file${a.blocking_count === 1 ? "" : "s"}, ${fmt_bytes(a.blocking)}`,
|
|
106
|
+
note: bl >= 90 ? "Little stands between the HTML and the first paint." : `${a.blocking_count} stylesheet(s) or sync script(s) in <head> (${fmt_bytes(a.blocking)}) must arrive before anything paints. Inline small CSS, merge the rest, load scripts as modules.`
|
|
107
|
+
});
|
|
108
|
+
} else missing.push({
|
|
109
|
+
key: "js",
|
|
110
|
+
label: "JS, page weight, render-blocking",
|
|
111
|
+
why: inp.assets_missing ?? "the page could not be weighed"
|
|
112
|
+
});
|
|
113
|
+
if (inp.htmlBytes !== null) {
|
|
114
|
+
const h = curve(inp.htmlBytes, CURVES.html[0], CURVES.html[1]);
|
|
115
|
+
cats.push({
|
|
116
|
+
key: "html",
|
|
117
|
+
label: "HTML document",
|
|
118
|
+
score: h,
|
|
119
|
+
weight: 5,
|
|
120
|
+
value: fmt_bytes(inp.htmlBytes),
|
|
121
|
+
note: h >= 90 ? "A compact document." : `The HTML is ${fmt_bytes(inp.htmlBytes)}: the browser parses all of it before it is interactive. The byte strip shows which part (markup, shadow roots, props, seed) is the bulk.`
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
{
|
|
125
|
+
const d = curve(inp.dataBytes, CURVES.data[0], CURVES.data[1]);
|
|
126
|
+
cats.push({
|
|
127
|
+
key: "data",
|
|
128
|
+
label: "Data shipped",
|
|
129
|
+
score: d,
|
|
130
|
+
weight: 10,
|
|
131
|
+
value: fmt_bytes(inp.dataBytes),
|
|
132
|
+
note: d >= 90 ? "Little data rides along for hydration." : `${fmt_bytes(inp.dataBytes)} of data (page seed, island props, inline script) ships so the browser can wake the page — serialized per request, parsed in the browser. Read fewer page.data keys in islands, pass smaller props.`,
|
|
133
|
+
...inp.dataDetail?.length ? { detail: inp.dataDetail } : {}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (inp.hasIslands && inp.browserSeen) {
|
|
137
|
+
const changed = inp.changed ?? 0;
|
|
138
|
+
const h = Math.round(100 * Math.exp(-(inp.recovered * .45 + inp.neverWoke * .35 + changed * .12)));
|
|
139
|
+
cats.push({
|
|
140
|
+
key: "hydration",
|
|
141
|
+
label: "Hydration integrity",
|
|
142
|
+
score: h,
|
|
143
|
+
weight: 15,
|
|
144
|
+
value: `${inp.recovered} recovered · ${inp.neverWoke} never woke${changed ? ` · ${changed} changed` : ""}`,
|
|
145
|
+
note: h >= 99 ? "Every island woke cleanly from its server markup." : `${inp.recovered} island(s) threw away their server HTML and rendered again (a flash + a double render), ${inp.neverWoke} never woke${changed ? `, ${changed} changed their markup as they woke` : ""}. The Islands table names each and why.`
|
|
146
|
+
});
|
|
147
|
+
} else if (inp.hasIslands) missing.push({
|
|
148
|
+
key: "hydration",
|
|
149
|
+
label: "Hydration integrity",
|
|
150
|
+
why: "no browser visit reported yet (open the page with the profiler on)"
|
|
151
|
+
});
|
|
152
|
+
if (inp.serverMs !== null) {
|
|
153
|
+
const s = curve(inp.serverMs, CURVES.server[0], CURVES.server[1]);
|
|
154
|
+
cats.push({
|
|
155
|
+
key: "server",
|
|
156
|
+
label: "Server render",
|
|
157
|
+
score: s,
|
|
158
|
+
weight: 15,
|
|
159
|
+
value: ms(inp.serverMs),
|
|
160
|
+
note: s >= 90 ? "The server renders the page quickly." : `${ms(inp.serverMs)} of server work before the first byte leaves. The forecast and findings above name what to cut.`
|
|
161
|
+
});
|
|
162
|
+
} else missing.push({
|
|
163
|
+
key: "server",
|
|
164
|
+
label: "Server render",
|
|
165
|
+
why: "no server timing in this report"
|
|
166
|
+
});
|
|
167
|
+
const v = inp.vitals;
|
|
168
|
+
const loading = [];
|
|
169
|
+
if (v?.lcp != null) loading.push({
|
|
170
|
+
s: curve(v.lcp, CURVES.lcp[0], CURVES.lcp[1]),
|
|
171
|
+
w: 60,
|
|
172
|
+
t: `LCP ${ms(v.lcp)}`
|
|
173
|
+
});
|
|
174
|
+
if (v?.fcp != null) loading.push({
|
|
175
|
+
s: curve(v.fcp, CURVES.fcp[0], CURVES.fcp[1]),
|
|
176
|
+
w: 25,
|
|
177
|
+
t: `FCP ${ms(v.fcp)}`
|
|
178
|
+
});
|
|
179
|
+
if (v?.ttfb != null) loading.push({
|
|
180
|
+
s: curve(v.ttfb, CURVES.ttfb[0], CURVES.ttfb[1]),
|
|
181
|
+
w: 15,
|
|
182
|
+
t: `TTFB ${ms(v.ttfb)}`
|
|
183
|
+
});
|
|
184
|
+
if (loading.length) {
|
|
185
|
+
const w = loading.reduce((x, p) => x + p.w, 0);
|
|
186
|
+
const s = Math.round(loading.reduce((x, p) => x + p.s * p.w, 0) / w);
|
|
187
|
+
cats.push({
|
|
188
|
+
key: "loading",
|
|
189
|
+
label: "Loading",
|
|
190
|
+
score: s,
|
|
191
|
+
weight: 10,
|
|
192
|
+
value: loading.map((p) => p.t).join(" · "),
|
|
193
|
+
note: s >= 90 ? "The page paints its main content quickly." : "The main content took long to paint in the visits measured. A slow first byte, blocking files, or a late-loading hero image are the usual causes.",
|
|
194
|
+
detail: loading.map((p) => `${p.t} → ${p.s}`)
|
|
195
|
+
});
|
|
196
|
+
} else missing.push({
|
|
197
|
+
key: "loading",
|
|
198
|
+
label: "Loading (LCP/FCP/TTFB)",
|
|
199
|
+
why: "no browser visit reported vitals"
|
|
200
|
+
});
|
|
201
|
+
if (v?.cls != null) {
|
|
202
|
+
const s = curve(v.cls, CURVES.cls[0], CURVES.cls[1]);
|
|
203
|
+
cats.push({
|
|
204
|
+
key: "stability",
|
|
205
|
+
label: "Visual stability",
|
|
206
|
+
score: s,
|
|
207
|
+
weight: 5,
|
|
208
|
+
value: `CLS ${v.cls}`,
|
|
209
|
+
note: s >= 90 ? "Nothing jumps around." : `Content moved as the page loaded (CLS ${v.cls}). Reserve space for images and islands; render islands at their final size on the server.`
|
|
210
|
+
});
|
|
211
|
+
} else missing.push({
|
|
212
|
+
key: "stability",
|
|
213
|
+
label: "Visual stability (CLS)",
|
|
214
|
+
why: inp.unmeasured?.cls ?? "no browser visit reported layout shifts"
|
|
215
|
+
});
|
|
216
|
+
const resp = [];
|
|
217
|
+
if (v?.inp != null) resp.push({
|
|
218
|
+
s: curve(v.inp, CURVES.inp[0], CURVES.inp[1]),
|
|
219
|
+
t: `INP ${ms(v.inp)}`
|
|
220
|
+
});
|
|
221
|
+
if (inp.tbt !== null) resp.push({
|
|
222
|
+
s: curve(inp.tbt, CURVES.tbt[0], CURVES.tbt[1]),
|
|
223
|
+
t: `blocking time ${ms(inp.tbt)}`
|
|
224
|
+
});
|
|
225
|
+
if (resp.length && inp.tbt === null && inp.unmeasured?.tbt) missing.push({
|
|
226
|
+
key: "responsiveness",
|
|
227
|
+
label: "Blocking time (part of responsiveness)",
|
|
228
|
+
why: inp.unmeasured.tbt
|
|
229
|
+
});
|
|
230
|
+
if (resp.length) {
|
|
231
|
+
const s = Math.round(resp.reduce((x, p) => x + p.s, 0) / resp.length);
|
|
232
|
+
cats.push({
|
|
233
|
+
key: "responsiveness",
|
|
234
|
+
label: "Responsiveness",
|
|
235
|
+
score: s,
|
|
236
|
+
weight: 10,
|
|
237
|
+
value: resp.map((p) => p.t).join(" · "),
|
|
238
|
+
note: (s >= 90 ? "The main thread stayed free to answer input in the visits measured." : "Long tasks held the main thread in the visits measured: input waited. Less JS at start, and lighter hydration, free it.") + " (Measured on the machine that visited: a slower phone pays more for the same JS — which JS at start scores.)",
|
|
239
|
+
detail: resp.map((p) => `${p.t} → ${p.s}`)
|
|
240
|
+
});
|
|
241
|
+
} else missing.push({
|
|
242
|
+
key: "responsiveness",
|
|
243
|
+
label: "Responsiveness (INP/blocking time)",
|
|
244
|
+
why: inp.unmeasured?.tbt ?? "no browser visit reported long tasks or interactions"
|
|
245
|
+
});
|
|
246
|
+
const total_weight = cats.reduce((s, c) => s + c.weight, 0);
|
|
247
|
+
const score = total_weight ? Math.round(cats.reduce((s, c) => s + c.score * c.weight, 0) / total_weight) : 0;
|
|
248
|
+
let worst = null;
|
|
249
|
+
for (const c of cats) {
|
|
250
|
+
c.lost = Math.round(c.weight / (total_weight || 1) * (100 - c.score) * 10) / 10;
|
|
251
|
+
if (c.lost > 0 && (!worst || c.lost > (worst.lost ?? 0))) worst = c;
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
score,
|
|
255
|
+
grade: grade_of(score),
|
|
256
|
+
categories: cats,
|
|
257
|
+
worst,
|
|
258
|
+
missing
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
export { CURVES, curve, fmt_bytes, page_score };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RequestEvent } from "@sveltejs/kit";
|
|
2
|
+
export declare const SESSION_COOKIE = "og_profiler";
|
|
3
|
+
/** Marks the navigation the login page makes right after a successful unlock: if THAT request still
|
|
4
|
+
* carries no session, the guard explains instead of sending the browser back to the login page. It
|
|
5
|
+
* carries no secret — only "a login just succeeded". */
|
|
6
|
+
export declare const AFTER_LOGIN_PARAM = "og-after-login";
|
|
7
|
+
/** What a request that failed auth carried (profiler/index.ts `#cookie_diagnosis`). */
|
|
8
|
+
export interface CookieDiagnosis {
|
|
9
|
+
/** a `Cookie` header arrived at all */
|
|
10
|
+
header: boolean;
|
|
11
|
+
/** its length in bytes (characters; cookie headers are ASCII) */
|
|
12
|
+
bytes: number;
|
|
13
|
+
/** how many `og_profiler=` pairs the raw header holds */
|
|
14
|
+
pairs: number;
|
|
15
|
+
/** whether Kit's own parser (`event.cookies.get`) found one */
|
|
16
|
+
parsed: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Every value of the cookie `name` in a raw `Cookie` header, in order — duplicates included (a
|
|
20
|
+
* browser sends two same-named cookies when their paths or domains differ; a parser that keeps one
|
|
21
|
+
* may keep the stale one). Percent-decoded where it decodes; kept raw where it does not, so one
|
|
22
|
+
* malformed neighbour cannot hide the session.
|
|
23
|
+
*/
|
|
24
|
+
export declare function raw_cookie_values(header: string, name: string): string[];
|
|
25
|
+
/** Every candidate session value on a request: Kit's parsed cookie first, then every raw pair. */
|
|
26
|
+
export declare function session_cookie_values(event: RequestEvent): string[];
|
|
27
|
+
/** The one-line account of a failed session, for the server log and the login page. */
|
|
28
|
+
export declare function describe_cookie_diagnosis(d: CookieDiagnosis): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/profiler/session-cookie.ts
|
|
2
|
+
const SESSION_COOKIE = "og_profiler";
|
|
3
|
+
/** Marks the navigation the login page makes right after a successful unlock: if THAT request still
|
|
4
|
+
* carries no session, the guard explains instead of sending the browser back to the login page. It
|
|
5
|
+
* carries no secret — only "a login just succeeded". */
|
|
6
|
+
const AFTER_LOGIN_PARAM = "og-after-login";
|
|
7
|
+
/**
|
|
8
|
+
* Every value of the cookie `name` in a raw `Cookie` header, in order — duplicates included (a
|
|
9
|
+
* browser sends two same-named cookies when their paths or domains differ; a parser that keeps one
|
|
10
|
+
* may keep the stale one). Percent-decoded where it decodes; kept raw where it does not, so one
|
|
11
|
+
* malformed neighbour cannot hide the session.
|
|
12
|
+
*/
|
|
13
|
+
function raw_cookie_values(header, name) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const part of header.split(";")) {
|
|
16
|
+
const eq = part.indexOf("=");
|
|
17
|
+
if (eq === -1 || part.slice(0, eq).trim() !== name) continue;
|
|
18
|
+
let value = part.slice(eq + 1).trim();
|
|
19
|
+
if (value.length >= 2 && value.startsWith("\"") && value.endsWith("\"")) value = value.slice(1, -1);
|
|
20
|
+
try {
|
|
21
|
+
value = decodeURIComponent(value);
|
|
22
|
+
} catch {}
|
|
23
|
+
out.push(value);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/** Every candidate session value on a request: Kit's parsed cookie first, then every raw pair. */
|
|
28
|
+
function session_cookie_values(event) {
|
|
29
|
+
const out = [];
|
|
30
|
+
const parsed = event.cookies.get(SESSION_COOKIE);
|
|
31
|
+
if (parsed) out.push(parsed);
|
|
32
|
+
const raw = event.request.headers.get("cookie");
|
|
33
|
+
if (raw) {
|
|
34
|
+
for (const v of raw_cookie_values(raw, SESSION_COOKIE)) if (v && !out.includes(v)) out.push(v);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** The one-line account of a failed session, for the server log and the login page. */
|
|
39
|
+
function describe_cookie_diagnosis(d) {
|
|
40
|
+
if (!d.header) return "Logged in, but no Cookie header reached the server at all — a proxy or CDN in front of the app is not forwarding cookies to this path.";
|
|
41
|
+
if (d.pairs === 0) return `Logged in, but your browser’s session cookie didn’t reach the server (Cookie header: ${d.bytes} bytes, no ${SESSION_COOKIE} in it) — a proxy or CDN dropped it, or the browser did not send it.`;
|
|
42
|
+
if (d.pairs > 1) return `Logged in, but your browser sent ${d.pairs} ${SESSION_COOKIE} cookies (Cookie header: ${d.bytes} bytes) and none is the current session — clear this site’s cookies for the profiler path and log in again.`;
|
|
43
|
+
return `Logged in, but the ${SESSION_COOKIE} cookie that arrived (Cookie header: ${d.bytes} bytes${d.parsed ? "" : ", not found by the cookie parser"}) is not a valid session — the profiler secret may differ between server instances.`;
|
|
44
|
+
}
|
|
45
|
+
export { AFTER_LOGIN_PARAM, SESSION_COOKIE, describe_cookie_diagnosis, raw_cookie_values, session_cookie_values };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SINK — how an ephemeral host keeps a picture of the whole site. A serverless instance lives
|
|
3
|
+
* for seconds; nothing it measures survives it. With `sink: { url }` each instance posts what it
|
|
4
|
+
* saw (one small row per request, one summary per sampler window, a caught request's dump) as
|
|
5
|
+
* NDJSON to a URL the app owns — an endpoint, a bucket — and the site view reads those rows back in
|
|
6
|
+
* the browser. Pure here: the rows, the buffer, and the aggregations the site view draws (the
|
|
7
|
+
* request cloud, the layer cake per route, the hot functions of a brushed selection).
|
|
8
|
+
*/
|
|
9
|
+
export type SinkRow = {
|
|
10
|
+
k: "req";
|
|
11
|
+
t: number;
|
|
12
|
+
path: string;
|
|
13
|
+
route: string | null;
|
|
14
|
+
ms: number;
|
|
15
|
+
cpu: number;
|
|
16
|
+
wait: number;
|
|
17
|
+
status: number;
|
|
18
|
+
og?: number;
|
|
19
|
+
} | {
|
|
20
|
+
k: "win";
|
|
21
|
+
t0: number;
|
|
22
|
+
t1: number;
|
|
23
|
+
fns: {
|
|
24
|
+
name: string;
|
|
25
|
+
file: string;
|
|
26
|
+
self_ms: number;
|
|
27
|
+
category: string;
|
|
28
|
+
}[];
|
|
29
|
+
reqs?: number;
|
|
30
|
+
} | {
|
|
31
|
+
k: "trap";
|
|
32
|
+
t: number;
|
|
33
|
+
path: string;
|
|
34
|
+
ms: number;
|
|
35
|
+
id: string;
|
|
36
|
+
dump?: unknown;
|
|
37
|
+
};
|
|
38
|
+
export declare class SinkBuffer {
|
|
39
|
+
#private;
|
|
40
|
+
readonly max_bytes: number;
|
|
41
|
+
constructor(max_bytes?: number);
|
|
42
|
+
push(row: SinkRow): void;
|
|
43
|
+
get size(): number;
|
|
44
|
+
/** everything buffered as NDJSON, and the buffer emptied */
|
|
45
|
+
drain(): string;
|
|
46
|
+
}
|
|
47
|
+
/** NDJSON (one or many posts concatenated) → rows; a bad line is skipped. */
|
|
48
|
+
export declare function parse_sink(text: string): SinkRow[];
|
|
49
|
+
export interface CloudPoint {
|
|
50
|
+
t: number;
|
|
51
|
+
ms: number;
|
|
52
|
+
route: string;
|
|
53
|
+
path: string;
|
|
54
|
+
status: number;
|
|
55
|
+
cpu: number;
|
|
56
|
+
wait: number;
|
|
57
|
+
}
|
|
58
|
+
export declare function cloud_points(rows: SinkRow[]): CloudPoint[];
|
|
59
|
+
export interface CakeRow {
|
|
60
|
+
route: string;
|
|
61
|
+
n: number;
|
|
62
|
+
/** ms summed over the route's requests */
|
|
63
|
+
cpu: number;
|
|
64
|
+
wait: number;
|
|
65
|
+
rest: number;
|
|
66
|
+
total: number;
|
|
67
|
+
p50: number;
|
|
68
|
+
p95: number;
|
|
69
|
+
}
|
|
70
|
+
/** Where the site's time goes, per route: CPU, waiting on calls, and the rest (the event loop,
|
|
71
|
+
* other requests in flight, the platform), summed and sorted by total. */
|
|
72
|
+
export declare function layer_cake(rows: SinkRow[]): CakeRow[];
|
|
73
|
+
/** The hot functions of the sampler windows that overlap [t0, t1] (a brushed selection of the
|
|
74
|
+
* cloud), merged by name + file and weighted by how much of each window is inside the range. */
|
|
75
|
+
export declare function hot_for_range(rows: SinkRow[], t0: number, t1: number, limit?: number): {
|
|
76
|
+
name: string;
|
|
77
|
+
file: string;
|
|
78
|
+
category: string;
|
|
79
|
+
self_ms: number;
|
|
80
|
+
windows: number;
|
|
81
|
+
}[];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// src/profiler/sink.ts
|
|
2
|
+
var SinkBuffer = class {
|
|
3
|
+
#rows = [];
|
|
4
|
+
#bytes = 0;
|
|
5
|
+
max_bytes;
|
|
6
|
+
constructor(max_bytes = 524288) {
|
|
7
|
+
this.max_bytes = max_bytes;
|
|
8
|
+
}
|
|
9
|
+
push(row) {
|
|
10
|
+
const line = JSON.stringify(row);
|
|
11
|
+
while (this.#bytes + line.length > this.max_bytes && this.#rows.length) {
|
|
12
|
+
const i = this.#rows.findIndex((r) => r.k === "req");
|
|
13
|
+
const [gone] = this.#rows.splice(i === -1 ? 0 : i, 1);
|
|
14
|
+
this.#bytes -= JSON.stringify(gone).length + 1;
|
|
15
|
+
}
|
|
16
|
+
this.#rows.push(row);
|
|
17
|
+
this.#bytes += line.length + 1;
|
|
18
|
+
}
|
|
19
|
+
get size() {
|
|
20
|
+
return this.#rows.length;
|
|
21
|
+
}
|
|
22
|
+
/** everything buffered as NDJSON, and the buffer emptied */
|
|
23
|
+
drain() {
|
|
24
|
+
const out = this.#rows.map((r) => JSON.stringify(r)).join("\n");
|
|
25
|
+
this.#rows = [];
|
|
26
|
+
this.#bytes = 0;
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** NDJSON (one or many posts concatenated) → rows; a bad line is skipped. */
|
|
31
|
+
function parse_sink(text) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const line of text.split("\n")) {
|
|
34
|
+
const s = line.trim();
|
|
35
|
+
if (!s) continue;
|
|
36
|
+
try {
|
|
37
|
+
const r = JSON.parse(s);
|
|
38
|
+
if (r && (r.k === "req" || r.k === "win" || r.k === "trap")) out.push(r);
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function cloud_points(rows) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const r of rows) if (r.k === "req") out.push({
|
|
46
|
+
t: r.t,
|
|
47
|
+
ms: r.ms,
|
|
48
|
+
route: r.route ?? r.path,
|
|
49
|
+
path: r.path,
|
|
50
|
+
status: r.status,
|
|
51
|
+
cpu: r.cpu,
|
|
52
|
+
wait: r.wait
|
|
53
|
+
});
|
|
54
|
+
return out.sort((a, b) => a.t - b.t);
|
|
55
|
+
}
|
|
56
|
+
/** Where the site's time goes, per route: CPU, waiting on calls, and the rest (the event loop,
|
|
57
|
+
* other requests in flight, the platform), summed and sorted by total. */
|
|
58
|
+
function layer_cake(rows) {
|
|
59
|
+
const by = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const r of rows) {
|
|
61
|
+
if (r.k !== "req") continue;
|
|
62
|
+
const key = r.route ?? r.path;
|
|
63
|
+
let e = by.get(key);
|
|
64
|
+
if (!e) by.set(key, e = {
|
|
65
|
+
ms: [],
|
|
66
|
+
cpu: 0,
|
|
67
|
+
wait: 0
|
|
68
|
+
});
|
|
69
|
+
e.ms.push(r.ms);
|
|
70
|
+
e.cpu += r.cpu;
|
|
71
|
+
e.wait += Math.min(r.wait, r.ms);
|
|
72
|
+
}
|
|
73
|
+
const pct = (xs, p) => {
|
|
74
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
75
|
+
return s[Math.min(s.length - 1, Math.floor(s.length * p))] ?? 0;
|
|
76
|
+
};
|
|
77
|
+
return [...by.entries()].map(([route, e]) => {
|
|
78
|
+
const total = e.ms.reduce((a, b) => a + b, 0);
|
|
79
|
+
const cpu = Math.min(e.cpu, total);
|
|
80
|
+
const wait = Math.min(e.wait, total - cpu);
|
|
81
|
+
return {
|
|
82
|
+
route,
|
|
83
|
+
n: e.ms.length,
|
|
84
|
+
cpu: r1(cpu),
|
|
85
|
+
wait: r1(wait),
|
|
86
|
+
rest: r1(total - cpu - wait),
|
|
87
|
+
total: r1(total),
|
|
88
|
+
p50: r1(pct(e.ms, .5)),
|
|
89
|
+
p95: r1(pct(e.ms, .95))
|
|
90
|
+
};
|
|
91
|
+
}).sort((a, b) => b.total - a.total);
|
|
92
|
+
}
|
|
93
|
+
const r1 = (n) => Math.round(n * 10) / 10;
|
|
94
|
+
/** The hot functions of the sampler windows that overlap [t0, t1] (a brushed selection of the
|
|
95
|
+
* cloud), merged by name + file and weighted by how much of each window is inside the range. */
|
|
96
|
+
function hot_for_range(rows, t0, t1, limit = 40) {
|
|
97
|
+
const by = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const r of rows) {
|
|
99
|
+
if (r.k !== "win" || r.t1 < t0 || r.t0 > t1) continue;
|
|
100
|
+
const span = Math.max(r.t1 - r.t0, 1);
|
|
101
|
+
const inside = Math.min(r.t1, t1) - Math.max(r.t0, t0);
|
|
102
|
+
const w = Math.max(0, Math.min(1, inside / span));
|
|
103
|
+
if (!w) continue;
|
|
104
|
+
for (const f of r.fns) {
|
|
105
|
+
const k = `${f.name}|${f.file}`;
|
|
106
|
+
const e = by.get(k);
|
|
107
|
+
if (e) {
|
|
108
|
+
e.self_ms += f.self_ms * w;
|
|
109
|
+
e.windows++;
|
|
110
|
+
} else by.set(k, {
|
|
111
|
+
name: f.name,
|
|
112
|
+
file: f.file,
|
|
113
|
+
category: f.category,
|
|
114
|
+
self_ms: f.self_ms * w,
|
|
115
|
+
windows: 1
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return [...by.values()].map((f) => ({
|
|
120
|
+
...f,
|
|
121
|
+
self_ms: r1(f.self_ms)
|
|
122
|
+
})).sort((a, b) => b.self_ms - a.self_ms).slice(0, limit);
|
|
123
|
+
}
|
|
124
|
+
export { SinkBuffer, cloud_points, hot_for_range, layer_cake, parse_sink };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Pattern, PatternKind } from "./patterns.js";
|
|
2
|
+
export interface SiteFix {
|
|
3
|
+
kind: PatternKind;
|
|
4
|
+
/** the pattern's title, without its "(N places)" */
|
|
5
|
+
title: string;
|
|
6
|
+
/** the line: its source module when the build had no sourcemap, else its file */
|
|
7
|
+
where: string;
|
|
8
|
+
line: number;
|
|
9
|
+
code: string;
|
|
10
|
+
/** per render on each page it slows, ms (biggest first) */
|
|
11
|
+
pages: {
|
|
12
|
+
page: string;
|
|
13
|
+
ms: number;
|
|
14
|
+
report: string;
|
|
15
|
+
}[];
|
|
16
|
+
/** the sum over those pages, ms per render (one render of each) */
|
|
17
|
+
total_ms: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function site_fixes(reports: readonly {
|
|
20
|
+
id: string;
|
|
21
|
+
page: string;
|
|
22
|
+
created: number;
|
|
23
|
+
runs: number;
|
|
24
|
+
patterns?: readonly Pattern[];
|
|
25
|
+
}[], limit?: number): SiteFix[];
|