@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,76 @@
|
|
|
1
|
+
// src/profiler/storage/index.ts
|
|
2
|
+
/** An in-memory store — a Map with an LRU cap. The profiler without a store keeps reports in its own
|
|
3
|
+
* memory already, so this adds nothing there; it is for tests and a model for a custom store.
|
|
4
|
+
* Nothing survives a restart. */
|
|
5
|
+
function memoryStore(cap = 50) {
|
|
6
|
+
const reports = /* @__PURE__ */ new Map();
|
|
7
|
+
const visits = [];
|
|
8
|
+
return {
|
|
9
|
+
kind: "memory",
|
|
10
|
+
putReport(rec) {
|
|
11
|
+
reports.delete(rec.id);
|
|
12
|
+
reports.set(rec.id, rec);
|
|
13
|
+
while (reports.size > cap) {
|
|
14
|
+
const oldest = reports.keys().next().value;
|
|
15
|
+
if (oldest === void 0) break;
|
|
16
|
+
reports.delete(oldest);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
getReport(id) {
|
|
20
|
+
return reports.get(id)?.dump ?? null;
|
|
21
|
+
},
|
|
22
|
+
listReports(limit = 30) {
|
|
23
|
+
return [...reports.values()].sort((a, b) => b.created - a.created).slice(0, limit).map(({ dump: _dump, ...s }) => s);
|
|
24
|
+
},
|
|
25
|
+
deleteReport(id) {
|
|
26
|
+
reports.delete(id);
|
|
27
|
+
},
|
|
28
|
+
prune(keep) {
|
|
29
|
+
while (reports.size > keep) {
|
|
30
|
+
const oldest = reports.keys().next().value;
|
|
31
|
+
if (oldest === void 0) break;
|
|
32
|
+
reports.delete(oldest);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
putVisit(rec) {
|
|
36
|
+
visits.push(rec);
|
|
37
|
+
if (visits.length > 2e3) visits.shift();
|
|
38
|
+
},
|
|
39
|
+
listVisits(page, limit = 10) {
|
|
40
|
+
return visits.filter((v) => v.page === page).sort((a, b) => b.at - a.at).slice(0, limit);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Pull the summary columns off a record for storing them alongside the dump. */
|
|
45
|
+
function recordColumns(rec) {
|
|
46
|
+
return {
|
|
47
|
+
id: rec.id,
|
|
48
|
+
created: rec.created,
|
|
49
|
+
page: rec.page,
|
|
50
|
+
trigger: rec.trigger,
|
|
51
|
+
label: rec.label,
|
|
52
|
+
median: rec.median
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Give the profiler its store AT RUNTIME. The profiler is configured in `vite.config.ts` (build
|
|
57
|
+
* time, serializable), but a store is a live object with a DB connection — so it is set here
|
|
58
|
+
* instead, once, before the first request:
|
|
59
|
+
*
|
|
60
|
+
* // src/hooks.server.ts
|
|
61
|
+
* import { setProfilerStore } from '@ogygia/devtools/profiler/storage';
|
|
62
|
+
* import { postgresStore } from '@ogygia/devtools/profiler/storage/postgres';
|
|
63
|
+
* setProfilerStore(postgresStore(process.env.DATABASE_URL!));
|
|
64
|
+
*
|
|
65
|
+
* The profiler reads this when it mounts and every finished report is written there. Call it before
|
|
66
|
+
* the profiler handles its first request; a later call is ignored (the instance already mounted).
|
|
67
|
+
*/
|
|
68
|
+
const STORE_SLOT = Symbol.for("ogygia.profiler.store.v1");
|
|
69
|
+
function setProfilerStore(store) {
|
|
70
|
+
globalThis[STORE_SLOT] = store;
|
|
71
|
+
}
|
|
72
|
+
/** The store set by {@link setProfilerStore}, if any — read by the profiler host on mount. */
|
|
73
|
+
function getProfilerStore() {
|
|
74
|
+
return globalThis[STORE_SLOT];
|
|
75
|
+
}
|
|
76
|
+
export { getProfilerStore, memoryStore, recordColumns, setProfilerStore };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ProfilerStore } from "./index.js";
|
|
2
|
+
interface PgLike {
|
|
3
|
+
query(text: string, params?: unknown[]): Promise<{
|
|
4
|
+
rows: Record<string, unknown>[];
|
|
5
|
+
}>;
|
|
6
|
+
end?(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare function postgresStore(client: PgLike | string, opts?: {
|
|
9
|
+
schema?: string;
|
|
10
|
+
table?: string;
|
|
11
|
+
}): ProfilerStore;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { MAX_VISITS_PER_KEY } from "./visits.js";
|
|
2
|
+
// src/profiler/storage/postgres.ts
|
|
3
|
+
async function resolve(client) {
|
|
4
|
+
if (typeof client !== "string") return client;
|
|
5
|
+
const pg = await import("pg");
|
|
6
|
+
const Pool = pg.Pool ?? pg.default?.Pool;
|
|
7
|
+
if (!Pool) throw new Error("ogygia/profiler: postgresStore needs the 'pg' package.");
|
|
8
|
+
return new Pool({ connectionString: client });
|
|
9
|
+
}
|
|
10
|
+
function postgresStore(client, opts = {}) {
|
|
11
|
+
const base = opts.table ?? "ogygia";
|
|
12
|
+
const q = (n) => opts.schema ? `"${opts.schema}"."${base}_${n}"` : `"${base}_${n}"`;
|
|
13
|
+
const R = q("reports");
|
|
14
|
+
const V = q("visits");
|
|
15
|
+
let c = null;
|
|
16
|
+
let ready = null;
|
|
17
|
+
const need = async () => {
|
|
18
|
+
c ??= await resolve(client);
|
|
19
|
+
ready ??= (async () => {
|
|
20
|
+
if (opts.schema) await c.query(`CREATE SCHEMA IF NOT EXISTS "${opts.schema}"`);
|
|
21
|
+
await c.query(`CREATE TABLE IF NOT EXISTS ${R} (id TEXT PRIMARY KEY, created BIGINT NOT NULL, page TEXT, trigger TEXT, label TEXT, median DOUBLE PRECISION, dump JSONB NOT NULL)`);
|
|
22
|
+
await c.query(`CREATE INDEX IF NOT EXISTS ${base}_reports_created ON ${R} (created DESC)`);
|
|
23
|
+
await c.query(`CREATE TABLE IF NOT EXISTS ${V} (key TEXT PRIMARY KEY, page TEXT NOT NULL, at BIGINT NOT NULL, visit JSONB NOT NULL)`);
|
|
24
|
+
await c.query(`CREATE INDEX IF NOT EXISTS ${base}_visits_page ON ${V} (page, at DESC)`);
|
|
25
|
+
})();
|
|
26
|
+
await ready;
|
|
27
|
+
return c;
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
kind: "postgres",
|
|
31
|
+
async init() {
|
|
32
|
+
await need();
|
|
33
|
+
},
|
|
34
|
+
async putReport(rec) {
|
|
35
|
+
await (await need()).query(`INSERT INTO ${R} (id,created,page,trigger,label,median,dump) VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb)
|
|
36
|
+
ON CONFLICT (id) DO UPDATE SET created=EXCLUDED.created,page=EXCLUDED.page,trigger=EXCLUDED.trigger,label=EXCLUDED.label,median=EXCLUDED.median,dump=EXCLUDED.dump`, [
|
|
37
|
+
rec.id,
|
|
38
|
+
rec.created,
|
|
39
|
+
rec.page,
|
|
40
|
+
rec.trigger,
|
|
41
|
+
rec.label,
|
|
42
|
+
rec.median,
|
|
43
|
+
rec.dump
|
|
44
|
+
]);
|
|
45
|
+
},
|
|
46
|
+
async getReport(id) {
|
|
47
|
+
const { rows } = await (await need()).query(`SELECT dump FROM ${R} WHERE id = $1`, [id]);
|
|
48
|
+
if (!rows.length) return null;
|
|
49
|
+
const d = rows[0].dump;
|
|
50
|
+
return typeof d === "string" ? d : JSON.stringify(d);
|
|
51
|
+
},
|
|
52
|
+
async listReports(limit = 30) {
|
|
53
|
+
const { rows } = await (await need()).query(`SELECT id,created,page,trigger,label,median FROM ${R} ORDER BY created DESC LIMIT $1`, [limit]);
|
|
54
|
+
return rows.map((r) => ({
|
|
55
|
+
id: r.id,
|
|
56
|
+
created: Number(r.created),
|
|
57
|
+
page: r.page ?? null,
|
|
58
|
+
trigger: r.trigger,
|
|
59
|
+
label: r.label,
|
|
60
|
+
median: r.median === null ? null : Number(r.median)
|
|
61
|
+
}));
|
|
62
|
+
},
|
|
63
|
+
async deleteReport(id) {
|
|
64
|
+
await (await need()).query(`DELETE FROM ${R} WHERE id = $1`, [id]);
|
|
65
|
+
},
|
|
66
|
+
async prune(keep) {
|
|
67
|
+
await (await need()).query(`DELETE FROM ${R} WHERE id NOT IN (SELECT id FROM ${R} ORDER BY created DESC LIMIT $1)`, [keep]);
|
|
68
|
+
},
|
|
69
|
+
async putVisit(rec) {
|
|
70
|
+
const db = await need();
|
|
71
|
+
await db.query(`INSERT INTO ${V} (key,page,at,visit) VALUES ($1,$2,$3,$4::jsonb) ON CONFLICT (key) DO UPDATE SET at=EXCLUDED.at,visit=EXCLUDED.visit`, [
|
|
72
|
+
rec.key,
|
|
73
|
+
rec.page,
|
|
74
|
+
rec.at,
|
|
75
|
+
rec.visit
|
|
76
|
+
]);
|
|
77
|
+
await db.query(`DELETE FROM ${V} WHERE page = $1 AND key NOT IN (SELECT key FROM ${V} WHERE page = $1 ORDER BY at DESC LIMIT $2)`, [rec.page, MAX_VISITS_PER_KEY]);
|
|
78
|
+
},
|
|
79
|
+
async listVisits(page, limit = 10) {
|
|
80
|
+
const { rows } = await (await need()).query(`SELECT key,page,at,visit FROM ${V} WHERE page = $1 ORDER BY at DESC LIMIT $2`, [page, limit]);
|
|
81
|
+
return rows.map((r) => ({
|
|
82
|
+
key: r.key,
|
|
83
|
+
page: r.page,
|
|
84
|
+
at: Number(r.at),
|
|
85
|
+
visit: typeof r.visit === "string" ? r.visit : JSON.stringify(r.visit)
|
|
86
|
+
}));
|
|
87
|
+
},
|
|
88
|
+
async close() {
|
|
89
|
+
await c?.end?.();
|
|
90
|
+
c = null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export { postgresStore };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ProfilerStore } from "./index.js";
|
|
2
|
+
/** The subset of a Redis client this store uses — node-redis and ioredis both satisfy it (their
|
|
3
|
+
* method names differ in case, and a few calls take options objects on one and arguments on the
|
|
4
|
+
* other; the flavour is read once, below). */
|
|
5
|
+
interface RedisLike {
|
|
6
|
+
set(key: string, val: string, ...rest: unknown[]): Promise<unknown>;
|
|
7
|
+
get(key: string): Promise<string | null>;
|
|
8
|
+
del(key: string | string[]): Promise<unknown>;
|
|
9
|
+
zadd?(key: string, ...rest: unknown[]): Promise<unknown>;
|
|
10
|
+
zrem?(key: string, ...members: string[]): Promise<unknown>;
|
|
11
|
+
zrange?(key: string, start: number, stop: number, ...rest: unknown[]): Promise<string[]>;
|
|
12
|
+
zcard?(key: string): Promise<number>;
|
|
13
|
+
connect?(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare function redisStore(client: RedisLike | string, opts?: {
|
|
16
|
+
ttlSeconds?: number;
|
|
17
|
+
prefix?: string;
|
|
18
|
+
}): ProfilerStore;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { MAX_VISITS_PER_KEY } from "./visits.js";
|
|
2
|
+
// src/profiler/storage/redis.ts
|
|
3
|
+
/** reports kept at most, whatever the host prunes to (a store used on its own still has a cap) */
|
|
4
|
+
const MAX_REPORTS = 1e3;
|
|
5
|
+
async function resolve(client) {
|
|
6
|
+
if (typeof client !== "string") return client;
|
|
7
|
+
const nodeRedis = "redis";
|
|
8
|
+
const ioRedis = "ioredis";
|
|
9
|
+
try {
|
|
10
|
+
const { createClient } = await import(nodeRedis);
|
|
11
|
+
const c = createClient({ url: client });
|
|
12
|
+
await c.connect?.();
|
|
13
|
+
return c;
|
|
14
|
+
} catch {}
|
|
15
|
+
return new (await (import(ioRedis))).default(client);
|
|
16
|
+
}
|
|
17
|
+
function redisStore(client, opts = {}) {
|
|
18
|
+
const ttl = opts.ttlSeconds;
|
|
19
|
+
const prefix = opts.prefix ?? "og:";
|
|
20
|
+
const R = `${prefix}report:`;
|
|
21
|
+
const RZ = `${prefix}reports`;
|
|
22
|
+
const V = `${prefix}visit:`;
|
|
23
|
+
const VZ = (page) => `${V}z:${page}`;
|
|
24
|
+
let c = null;
|
|
25
|
+
const need = async () => c ??= await resolve(client);
|
|
26
|
+
const node = (r) => typeof r.zAdd === "function" ? r : null;
|
|
27
|
+
const setStr = async (r, key, val) => {
|
|
28
|
+
const n = node(r);
|
|
29
|
+
if (!ttl) await r.set(key, val);
|
|
30
|
+
else if (n) await n.set(key, val, { EX: ttl });
|
|
31
|
+
else await r.set(key, val, "EX", ttl);
|
|
32
|
+
};
|
|
33
|
+
const zadd = (r, key, score, member) => {
|
|
34
|
+
const n = node(r);
|
|
35
|
+
return n ? n.zAdd(key, {
|
|
36
|
+
score,
|
|
37
|
+
value: member
|
|
38
|
+
}) : r.zadd(key, score, member);
|
|
39
|
+
};
|
|
40
|
+
const zrem = (r, key, members) => {
|
|
41
|
+
if (!members.length) return Promise.resolve();
|
|
42
|
+
const n = node(r);
|
|
43
|
+
return n ? n.zRem(key, members) : r.zrem(key, ...members);
|
|
44
|
+
};
|
|
45
|
+
const zrange = (r, key, a, b, rev = false) => {
|
|
46
|
+
const n = node(r);
|
|
47
|
+
if (n) return rev ? n.zRange(key, a, b, { REV: true }) : n.zRange(key, a, b);
|
|
48
|
+
return rev ? r.zrange(key, a, b, "REV") : r.zrange(key, a, b);
|
|
49
|
+
};
|
|
50
|
+
const zcard = (r, key) => {
|
|
51
|
+
const n = node(r);
|
|
52
|
+
return n ? n.zCard(key) : r.zcard(key);
|
|
53
|
+
};
|
|
54
|
+
/** drop every member but the newest `keep`, with the keys they name */
|
|
55
|
+
const trim = async (r, key, item, keep) => {
|
|
56
|
+
const stale = await zrange(r, key, 0, -(keep + 1));
|
|
57
|
+
if (!stale.length) return;
|
|
58
|
+
await r.del(stale.map((m) => item + m));
|
|
59
|
+
await zrem(r, key, stale);
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
kind: "redis",
|
|
63
|
+
async init() {
|
|
64
|
+
await need();
|
|
65
|
+
},
|
|
66
|
+
async putReport(rec) {
|
|
67
|
+
const r = await need();
|
|
68
|
+
const summary = {
|
|
69
|
+
id: rec.id,
|
|
70
|
+
created: rec.created,
|
|
71
|
+
page: rec.page,
|
|
72
|
+
trigger: rec.trigger,
|
|
73
|
+
label: rec.label,
|
|
74
|
+
median: rec.median
|
|
75
|
+
};
|
|
76
|
+
await setStr(r, R + rec.id, JSON.stringify({
|
|
77
|
+
summary,
|
|
78
|
+
dump: rec.dump
|
|
79
|
+
}));
|
|
80
|
+
await zadd(r, RZ, rec.created, rec.id);
|
|
81
|
+
if (await zcard(r, RZ) > MAX_REPORTS) await trim(r, RZ, R, MAX_REPORTS);
|
|
82
|
+
},
|
|
83
|
+
async getReport(id) {
|
|
84
|
+
const raw = await (await need()).get(R + id);
|
|
85
|
+
if (!raw) return null;
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(raw).dump;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
async listReports(limit = 30) {
|
|
93
|
+
const r = await need();
|
|
94
|
+
const ids = await zrange(r, RZ, 0, limit - 1, true);
|
|
95
|
+
const out = [];
|
|
96
|
+
const gone = [];
|
|
97
|
+
for (const id of ids) {
|
|
98
|
+
const raw = await r.get(R + id);
|
|
99
|
+
if (!raw) {
|
|
100
|
+
gone.push(id);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
out.push(JSON.parse(raw).summary);
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
await zrem(r, RZ, gone);
|
|
108
|
+
return out;
|
|
109
|
+
},
|
|
110
|
+
async deleteReport(id) {
|
|
111
|
+
const r = await need();
|
|
112
|
+
await r.del(R + id);
|
|
113
|
+
await zrem(r, RZ, [id]);
|
|
114
|
+
},
|
|
115
|
+
async prune(keep) {
|
|
116
|
+
await trim(await need(), RZ, R, keep);
|
|
117
|
+
},
|
|
118
|
+
async putVisit(rec) {
|
|
119
|
+
const r = await need();
|
|
120
|
+
await setStr(r, V + rec.key, JSON.stringify(rec));
|
|
121
|
+
await zadd(r, VZ(rec.page), rec.at, rec.key);
|
|
122
|
+
if (await zcard(r, VZ(rec.page)) > 2e3) await trim(r, VZ(rec.page), V, MAX_VISITS_PER_KEY);
|
|
123
|
+
},
|
|
124
|
+
async listVisits(page, limit = 10) {
|
|
125
|
+
const r = await need();
|
|
126
|
+
const keys = await zrange(r, VZ(page), 0, limit - 1, true);
|
|
127
|
+
const out = [];
|
|
128
|
+
const gone = [];
|
|
129
|
+
for (const k of keys) {
|
|
130
|
+
const raw = await r.get(V + k);
|
|
131
|
+
if (!raw) {
|
|
132
|
+
gone.push(k);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
out.push(JSON.parse(raw));
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
await zrem(r, VZ(page), gone);
|
|
140
|
+
return out;
|
|
141
|
+
},
|
|
142
|
+
async close() {
|
|
143
|
+
await c?.quit?.().catch(() => {});
|
|
144
|
+
c = null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export { redisStore };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { MAX_VISITS_PER_KEY } from "./visits.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
// src/profiler/storage/sqlite.ts
|
|
4
|
+
/**
|
|
5
|
+
* SQLite report store — a file (or `:memory:`). The simplest durable backend: one file, no server,
|
|
6
|
+
* survives a restart. Uses Node's built-in `node:sqlite` (Node 22.5+) when present, else
|
|
7
|
+
* `better-sqlite3` if installed. Both are synchronous, which is fine here: a put is one small
|
|
8
|
+
* insert, a get one indexed lookup, off the request's hot path (a report is written once, after
|
|
9
|
+
* the recording).
|
|
10
|
+
*
|
|
11
|
+
* // src/hooks.server.ts
|
|
12
|
+
* import { setProfilerStore } from '@ogygia/devtools/profiler/storage';
|
|
13
|
+
* import { sqliteStore } from '@ogygia/devtools/profiler/storage/sqlite';
|
|
14
|
+
* setProfilerStore(sqliteStore('.ogygia/profiles.db'));
|
|
15
|
+
*/
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
/** Open a synchronous sqlite database from whichever driver is installed. */
|
|
18
|
+
function open(filename) {
|
|
19
|
+
if (filename !== ":memory:" && !filename.startsWith("file::memory:")) try {
|
|
20
|
+
const fs = require("node:fs");
|
|
21
|
+
const dir = require("node:path").dirname(filename);
|
|
22
|
+
if (dir && dir !== ".") fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
} catch {}
|
|
24
|
+
try {
|
|
25
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
26
|
+
return new DatabaseSync(filename);
|
|
27
|
+
} catch {}
|
|
28
|
+
try {
|
|
29
|
+
return new (require("better-sqlite3"))(filename);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error("ogygia/profiler: sqliteStore needs Node 22.5+ (built-in node:sqlite) or the 'better-sqlite3' package.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function sqliteStore(filename = ".ogygia/profiles.db") {
|
|
35
|
+
let db = null;
|
|
36
|
+
const need = () => {
|
|
37
|
+
if (db) return db;
|
|
38
|
+
db = open(filename);
|
|
39
|
+
db.exec(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS reports (
|
|
41
|
+
id TEXT PRIMARY KEY, created INTEGER NOT NULL, page TEXT, trigger TEXT,
|
|
42
|
+
label TEXT, median REAL, dump TEXT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS reports_created ON reports (created);
|
|
45
|
+
CREATE TABLE IF NOT EXISTS visits (
|
|
46
|
+
key TEXT PRIMARY KEY, page TEXT NOT NULL, at INTEGER NOT NULL, visit TEXT NOT NULL
|
|
47
|
+
);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS visits_page ON visits (page, at);
|
|
49
|
+
`);
|
|
50
|
+
return db;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
kind: "sqlite",
|
|
54
|
+
init() {
|
|
55
|
+
need();
|
|
56
|
+
},
|
|
57
|
+
putReport(rec) {
|
|
58
|
+
need().prepare("INSERT INTO reports (id,created,page,trigger,label,median,dump) VALUES (?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET created=excluded.created,page=excluded.page,trigger=excluded.trigger,label=excluded.label,median=excluded.median,dump=excluded.dump").run(rec.id, rec.created, rec.page, rec.trigger, rec.label, rec.median, rec.dump);
|
|
59
|
+
},
|
|
60
|
+
getReport(id) {
|
|
61
|
+
return need().prepare("SELECT dump FROM reports WHERE id = ?").get(id)?.dump ?? null;
|
|
62
|
+
},
|
|
63
|
+
listReports(limit = 30) {
|
|
64
|
+
return need().prepare("SELECT id,created,page,trigger,label,median FROM reports ORDER BY created DESC LIMIT ?").all(limit);
|
|
65
|
+
},
|
|
66
|
+
deleteReport(id) {
|
|
67
|
+
need().prepare("DELETE FROM reports WHERE id = ?").run(id);
|
|
68
|
+
},
|
|
69
|
+
prune(keep) {
|
|
70
|
+
need().prepare("DELETE FROM reports WHERE id NOT IN (SELECT id FROM reports ORDER BY created DESC LIMIT ?)").run(keep);
|
|
71
|
+
},
|
|
72
|
+
putVisit(rec) {
|
|
73
|
+
need().prepare("INSERT INTO visits (key,page,at,visit) VALUES (?,?,?,?) ON CONFLICT(key) DO UPDATE SET at=excluded.at,visit=excluded.visit").run(rec.key, rec.page, rec.at, rec.visit);
|
|
74
|
+
need().prepare("DELETE FROM visits WHERE page = ? AND key NOT IN (SELECT key FROM visits WHERE page = ? ORDER BY at DESC LIMIT ?)").run(rec.page, rec.page, MAX_VISITS_PER_KEY);
|
|
75
|
+
},
|
|
76
|
+
listVisits(page, limit = 10) {
|
|
77
|
+
return need().prepare("SELECT key,page,at,visit FROM visits WHERE page = ? ORDER BY at DESC LIMIT ?").all(page, limit);
|
|
78
|
+
},
|
|
79
|
+
close() {
|
|
80
|
+
db?.close();
|
|
81
|
+
db = null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export { sqliteStore };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The most visit rows a bundled store keeps under one `page` key, newest first. A server window
|
|
2
|
+
* writes every request it saw (from any instance) under ONE key and reads up to this many back,
|
|
3
|
+
* so the store's retention and the window's read are the same number. */
|
|
4
|
+
export declare const MAX_VISITS_PER_KEY = 2e3;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// src/profiler/storage/visits.ts
|
|
2
|
+
/** The most visit rows a bundled store keeps under one `page` key, newest first. A server window
|
|
3
|
+
* writes every request it saw (from any instance) under ONE key and reads up to this many back,
|
|
4
|
+
* so the store's retention and the window's read are the same number. */
|
|
5
|
+
const MAX_VISITS_PER_KEY = 2e3;
|
|
6
|
+
export { MAX_VISITS_PER_KEY };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ByteStrip, StripKind } from "./byte-strip.js";
|
|
2
|
+
export interface StreamTail {
|
|
3
|
+
/** the early part: its last byte left the server at `early_ms` (ms into the render) */
|
|
4
|
+
early_ms: number;
|
|
5
|
+
early_bytes: number;
|
|
6
|
+
/** the tail: its last byte left at `late_ms` */
|
|
7
|
+
late_ms: number;
|
|
8
|
+
late_bytes: number;
|
|
9
|
+
/** each streamed promise that settled in the tail, in the order it left: its `page.data` key
|
|
10
|
+
* (a dotted path one level deep; null when the seed cannot say) and when */
|
|
11
|
+
keys: {
|
|
12
|
+
key: string | null;
|
|
13
|
+
id: number;
|
|
14
|
+
left_ms: number;
|
|
15
|
+
}[];
|
|
16
|
+
/** the tail's other bytes by what they are (an island's markup: its fingerprint) */
|
|
17
|
+
other: {
|
|
18
|
+
kind: StripKind;
|
|
19
|
+
label?: string;
|
|
20
|
+
bytes: number;
|
|
21
|
+
}[];
|
|
22
|
+
}
|
|
23
|
+
export declare function stream_tail(html: string, strip: ByteStrip, chunks: {
|
|
24
|
+
end: number;
|
|
25
|
+
t: number;
|
|
26
|
+
}[]): StreamTail | null;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { left_at } from "./byte-strip.js";
|
|
2
|
+
import { defer_keys } from "@ogygia/core/insights";
|
|
3
|
+
import { PAGE_DEFER_GLOBAL, PAGE_DEFER_KEY } from "@ogygia/core/internal/instruments";
|
|
4
|
+
// src/profiler/stream-tail.ts
|
|
5
|
+
/**
|
|
6
|
+
* WHAT HELD THE DOCUMENT OPEN — a streamed page ships its shell, then keeps the response open until
|
|
7
|
+
* its last streamed promise settles (a `<script>__ogygia_page_resolve(id, …)<\/script>` per settle,
|
|
8
|
+
* after `</html>`). The browser fires DOMContentLoaded only when the document ends, and islands wake
|
|
9
|
+
* after it, so the slowest streamed promise holds every island on the page. Read off the profiled
|
|
10
|
+
* render's body: the biggest pause between chunks splits the early part from the late tail, and each
|
|
11
|
+
* resolve script in the tail is named by the `page.data` key its id stands for (page-insights
|
|
12
|
+
* `defer_keys`, the Page tab's own reading). Pure; indexOf only.
|
|
13
|
+
*/
|
|
14
|
+
/** a pause between chunks shorter than this is the server writing, not waiting */
|
|
15
|
+
const MIN_PAUSE_MS = 100;
|
|
16
|
+
function stream_tail(html, strip, chunks) {
|
|
17
|
+
if (chunks.length < 2) return null;
|
|
18
|
+
let split = -1;
|
|
19
|
+
let pause = 0;
|
|
20
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
21
|
+
const d = chunks[i].t - chunks[i - 1].t;
|
|
22
|
+
if (d > pause) {
|
|
23
|
+
pause = d;
|
|
24
|
+
split = i;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (split === -1 || pause < MIN_PAUSE_MS) return null;
|
|
28
|
+
const early_end = chunks[split - 1].end;
|
|
29
|
+
const last = chunks[chunks.length - 1];
|
|
30
|
+
const names = defer_keys(html, PAGE_DEFER_KEY);
|
|
31
|
+
const keys = [];
|
|
32
|
+
const call = `${PAGE_DEFER_GLOBAL}(`;
|
|
33
|
+
const resolves = [];
|
|
34
|
+
for (let at = html.indexOf(call, early_end); at !== -1; at = html.indexOf(call, at + call.length)) {
|
|
35
|
+
let n = 0;
|
|
36
|
+
let j = at + call.length;
|
|
37
|
+
let digits = 0;
|
|
38
|
+
for (; j < html.length && html.charCodeAt(j) >= 48 && html.charCodeAt(j) <= 57; j++, digits++) n = n * 10 + (html.charCodeAt(j) - 48);
|
|
39
|
+
if (!digits) continue;
|
|
40
|
+
keys.push({
|
|
41
|
+
key: names.get(n) ?? null,
|
|
42
|
+
id: n,
|
|
43
|
+
left_ms: left_at(at, chunks) ?? last.t
|
|
44
|
+
});
|
|
45
|
+
resolves.push({
|
|
46
|
+
start: at,
|
|
47
|
+
end: html.indexOf("<\/script", at)
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const in_resolve = (s) => resolves.some((r) => s.start <= r.start && r.end <= s.end);
|
|
51
|
+
const other = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const s of strip.segments) {
|
|
53
|
+
const from = Math.max(s.start, early_end);
|
|
54
|
+
if (s.end <= from || in_resolve(s)) continue;
|
|
55
|
+
const k = `${s.kind}|${s.label ?? ""}`;
|
|
56
|
+
const o = other.get(k) ?? {
|
|
57
|
+
kind: s.kind,
|
|
58
|
+
...s.label ? { label: s.label } : {},
|
|
59
|
+
bytes: 0
|
|
60
|
+
};
|
|
61
|
+
o.bytes += s.end - from;
|
|
62
|
+
other.set(k, o);
|
|
63
|
+
}
|
|
64
|
+
keys.sort((a, b) => a.left_ms - b.left_ms);
|
|
65
|
+
return {
|
|
66
|
+
early_ms: chunks[split - 1].t,
|
|
67
|
+
early_bytes: early_end,
|
|
68
|
+
late_ms: last.t,
|
|
69
|
+
late_bytes: html.length - early_end,
|
|
70
|
+
keys,
|
|
71
|
+
other: [...other.values()].sort((a, b) => b.bytes - a.bytes)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export { stream_tail };
|