@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,10 @@
|
|
|
1
|
+
/** a text answer at least this big goes out gzipped (see `gzip_large`) */
|
|
2
|
+
export declare const GZIP_OVER_BYTES: number;
|
|
3
|
+
/**
|
|
4
|
+
* A big profiler answer (a report page is MBs of island props; the run page's `keep` answer carries
|
|
5
|
+
* the whole report) gzipped when the browser takes it: AWS Lambda — Amplify's SSR — caps a response
|
|
6
|
+
* at 6 MB before any CDN compresses it, and a heavy page's report crossed that and failed. Text only
|
|
7
|
+
* (HTML, JSON), never a response that is already encoded, and never the profiler's own in-process
|
|
8
|
+
* fetch (Kit hands that Response back as it is, and the standalone export reads it as text).
|
|
9
|
+
*/
|
|
10
|
+
export declare function gzip_large(req: Request, res: Response): Promise<Response>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/profiler/compress.ts
|
|
2
|
+
/** a text answer at least this big goes out gzipped (see `gzip_large`) */
|
|
3
|
+
const GZIP_OVER_BYTES = 262144;
|
|
4
|
+
/**
|
|
5
|
+
* A big profiler answer (a report page is MBs of island props; the run page's `keep` answer carries
|
|
6
|
+
* the whole report) gzipped when the browser takes it: AWS Lambda — Amplify's SSR — caps a response
|
|
7
|
+
* at 6 MB before any CDN compresses it, and a heavy page's report crossed that and failed. Text only
|
|
8
|
+
* (HTML, JSON), never a response that is already encoded, and never the profiler's own in-process
|
|
9
|
+
* fetch (Kit hands that Response back as it is, and the standalone export reads it as text).
|
|
10
|
+
*/
|
|
11
|
+
async function gzip_large(req, res) {
|
|
12
|
+
if (!res.body || res.headers.has("content-encoding") || req.headers.get("x-og-profiler-internal")) return res;
|
|
13
|
+
if (!(req.headers.get("accept-encoding") ?? "").includes("gzip")) return res;
|
|
14
|
+
const type = res.headers.get("content-type") ?? "";
|
|
15
|
+
if (!type.startsWith("text/") && !type.includes("json")) return res;
|
|
16
|
+
const known = Number(res.headers.get("content-length"));
|
|
17
|
+
if (known && known < 262144) return res;
|
|
18
|
+
const body = new Uint8Array(await res.arrayBuffer());
|
|
19
|
+
const headers = new Headers(res.headers);
|
|
20
|
+
if (body.byteLength < 262144) return new Response(body, {
|
|
21
|
+
status: res.status,
|
|
22
|
+
statusText: res.statusText,
|
|
23
|
+
headers
|
|
24
|
+
});
|
|
25
|
+
const { gzip } = await import("node:zlib");
|
|
26
|
+
const zipped = await new Promise((resolve, reject) => gzip(body, { level: 6 }, (err, out) => err ? reject(err) : resolve(new Uint8Array(out.buffer, out.byteOffset, out.byteLength))));
|
|
27
|
+
headers.set("content-encoding", "gzip");
|
|
28
|
+
headers.set("content-length", String(zipped.byteLength));
|
|
29
|
+
headers.append("vary", "accept-encoding");
|
|
30
|
+
return new Response(zipped, {
|
|
31
|
+
status: res.status,
|
|
32
|
+
statusText: res.statusText,
|
|
33
|
+
headers
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export { GZIP_OVER_BYTES, gzip_large };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { RequestEntry } from "./report.js";
|
|
2
|
+
export interface ContendingRequest {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
route: string | null;
|
|
6
|
+
status: number;
|
|
7
|
+
ms: number;
|
|
8
|
+
/** ms of its lifetime inside the profiled window(s) */
|
|
9
|
+
overlap_ms: number;
|
|
10
|
+
/** at most this much of the window's CPU was its (upper bound) */
|
|
11
|
+
cpu_max_ms: number;
|
|
12
|
+
/** `self`: a call the profiled render made to its own server (the same event loop answered
|
|
13
|
+
* both sides); `hole`: one of the page's own deferred holes (the page's work, answered while
|
|
14
|
+
* it rendered); `other`: a request from somewhere else */
|
|
15
|
+
kind: "self" | "hole" | "other";
|
|
16
|
+
/** a remote function call (a `self` one: a render reading a `prerender` function's answer) */
|
|
17
|
+
remote?: true;
|
|
18
|
+
}
|
|
19
|
+
export interface Contention {
|
|
20
|
+
/** the heaviest overlaps, at most 40 (count with `counts`, not this list) */
|
|
21
|
+
requests: ContendingRequest[];
|
|
22
|
+
/** every overlapping request tallied by kind, before the list was cut (absent on older reports) */
|
|
23
|
+
counts?: {
|
|
24
|
+
self: number;
|
|
25
|
+
self_ms: number;
|
|
26
|
+
self_paths: string[];
|
|
27
|
+
self_remote?: number;
|
|
28
|
+
other: number;
|
|
29
|
+
hole: number;
|
|
30
|
+
};
|
|
31
|
+
/** the sum of the overlaps (a wall-time measure: two requests over one ms count twice) */
|
|
32
|
+
overlap_ms: number;
|
|
33
|
+
/** the upper bound on CPU the window lost to them, summed */
|
|
34
|
+
cpu_max_ms: number;
|
|
35
|
+
/** how many were in flight when the profiled render(s) started, from the render's own entry */
|
|
36
|
+
inflight_at_start: number[];
|
|
37
|
+
/** per profiled window: how many others overlapped it and for how long */
|
|
38
|
+
per_window: {
|
|
39
|
+
n: number;
|
|
40
|
+
overlap_ms: number;
|
|
41
|
+
cpu_max_ms: number;
|
|
42
|
+
}[];
|
|
43
|
+
/** the share of the windows' total length that had another request in flight */
|
|
44
|
+
busy_share: number;
|
|
45
|
+
}
|
|
46
|
+
export interface ContentionInput {
|
|
47
|
+
/** the ring, any order */
|
|
48
|
+
requests: readonly RequestEntry[];
|
|
49
|
+
/** the profiled window(s) on performance.now()'s clock */
|
|
50
|
+
windows: readonly {
|
|
51
|
+
start: number;
|
|
52
|
+
end: number;
|
|
53
|
+
}[];
|
|
54
|
+
/** the profiled renders' own ring entries (internal), to read `inflight` off */
|
|
55
|
+
own?: readonly RequestEntry[];
|
|
56
|
+
/** the paths the profiled render called on its own server (its outbound calls' pathnames):
|
|
57
|
+
* a ring entry on one of them is the render waiting on itself, not a stranger */
|
|
58
|
+
self_paths?: ReadonlySet<string>;
|
|
59
|
+
}
|
|
60
|
+
export declare function contention(input: ContentionInput): Contention | undefined;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { islands_endpoint } from "@ogygia/core/internal/server";
|
|
2
|
+
// src/profiler/contention.ts
|
|
3
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
4
|
+
function contention(input) {
|
|
5
|
+
const windows = input.windows;
|
|
6
|
+
if (!windows.length) return void 0;
|
|
7
|
+
const per_window = windows.map(() => ({
|
|
8
|
+
n: 0,
|
|
9
|
+
overlap_ms: 0,
|
|
10
|
+
cpu_max_ms: 0
|
|
11
|
+
}));
|
|
12
|
+
const rows = [];
|
|
13
|
+
let overlap_total = 0;
|
|
14
|
+
let cpu_total = 0;
|
|
15
|
+
const busy = [];
|
|
16
|
+
for (const e of input.requests) {
|
|
17
|
+
if (e.internal || e.pt === void 0) continue;
|
|
18
|
+
const s = e.pt;
|
|
19
|
+
const t = e.pt + e.ms;
|
|
20
|
+
let overlap = 0;
|
|
21
|
+
for (let i = 0; i < windows.length; i++) {
|
|
22
|
+
const w = windows[i];
|
|
23
|
+
const ov = Math.min(t, w.end) - Math.max(s, w.start);
|
|
24
|
+
if (ov <= 0) continue;
|
|
25
|
+
overlap += ov;
|
|
26
|
+
per_window[i].n++;
|
|
27
|
+
per_window[i].overlap_ms += ov;
|
|
28
|
+
busy.push([Math.max(s, w.start), Math.min(t, w.end)]);
|
|
29
|
+
}
|
|
30
|
+
if (overlap <= 0) continue;
|
|
31
|
+
const cpu_max = Math.min(overlap, e.ms > 0 ? e.cpu_ms * (overlap / e.ms) : 0);
|
|
32
|
+
for (let i = 0; i < windows.length; i++) {
|
|
33
|
+
const w = windows[i];
|
|
34
|
+
const ov = Math.min(t, w.end) - Math.max(s, w.start);
|
|
35
|
+
if (ov > 0) per_window[i].cpu_max_ms += cpu_max * (ov / overlap);
|
|
36
|
+
}
|
|
37
|
+
overlap_total += overlap;
|
|
38
|
+
cpu_total += cpu_max;
|
|
39
|
+
rows.push({
|
|
40
|
+
method: e.method,
|
|
41
|
+
path: e.path,
|
|
42
|
+
route: e.route,
|
|
43
|
+
status: e.status,
|
|
44
|
+
ms: round2(e.ms),
|
|
45
|
+
overlap_ms: round2(overlap),
|
|
46
|
+
cpu_max_ms: round2(cpu_max),
|
|
47
|
+
kind: e.hole || e.path.endsWith(islands_endpoint()) ? "hole" : input.self_paths?.has(e.path) ? "self" : "other",
|
|
48
|
+
...e.remote ? { remote: true } : {}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const inflight = (input.own ?? []).filter((e) => e.internal && typeof e.inflight === "number").map((e) => e.inflight);
|
|
52
|
+
busy.sort((a, b) => a[0] - b[0]);
|
|
53
|
+
let union = 0;
|
|
54
|
+
let cur = null;
|
|
55
|
+
for (const iv of busy) if (cur && iv[0] <= cur[1]) cur[1] = Math.max(cur[1], iv[1]);
|
|
56
|
+
else {
|
|
57
|
+
if (cur) union += cur[1] - cur[0];
|
|
58
|
+
cur = iv;
|
|
59
|
+
}
|
|
60
|
+
if (cur) union += cur[1] - cur[0];
|
|
61
|
+
const total_len = windows.reduce((a, w) => a + Math.max(0, w.end - w.start), 0) || 1;
|
|
62
|
+
if (!rows.length && !inflight.some((n) => n > 0)) return void 0;
|
|
63
|
+
rows.sort((a, b) => b.overlap_ms - a.overlap_ms);
|
|
64
|
+
const self_rows = rows.filter((r) => r.kind === "self");
|
|
65
|
+
return {
|
|
66
|
+
requests: rows.slice(0, 40),
|
|
67
|
+
counts: {
|
|
68
|
+
self: self_rows.length,
|
|
69
|
+
self_ms: round2(self_rows.reduce((s, r) => s + r.ms, 0)),
|
|
70
|
+
self_paths: [...new Set(self_rows.map((r) => r.path))].slice(0, 5),
|
|
71
|
+
self_remote: self_rows.filter((r) => r.remote).length,
|
|
72
|
+
other: rows.filter((r) => r.kind === "other").length,
|
|
73
|
+
hole: rows.filter((r) => r.kind === "hole").length
|
|
74
|
+
},
|
|
75
|
+
overlap_ms: round2(overlap_total),
|
|
76
|
+
cpu_max_ms: round2(cpu_total),
|
|
77
|
+
inflight_at_start: inflight,
|
|
78
|
+
per_window: per_window.map((p) => ({
|
|
79
|
+
n: p.n,
|
|
80
|
+
overlap_ms: round2(p.overlap_ms),
|
|
81
|
+
cpu_max_ms: round2(p.cpu_max_ms)
|
|
82
|
+
})),
|
|
83
|
+
busy_share: round2(Math.min(1, union / total_len))
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export { contention };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** True when `bytes` starts with the `.ogp` magic (so import can tell it from legacy plain JSON). */
|
|
2
|
+
export declare function is_ogp(bytes: Uint8Array): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Undo the two ways a `.ogp` gets mangled moving BETWEEN machines through a text-y channel (a shared
|
|
5
|
+
* clipboard, an editor re-save, a chat) — the file arrives byte-perfect on either side of the codec,
|
|
6
|
+
* so the raw magic is intact, but its wrapper isn't:
|
|
7
|
+
* - a **UTF-8 BOM** prepended (`EF BB BF`) — strip it;
|
|
8
|
+
* - the whole file **base64-encoded** — decode it.
|
|
9
|
+
* A real `.ogp` already starts with the magic, so this is a no-op on it. Returns bytes to hand to
|
|
10
|
+
* `is_ogp` / `ogp_decode`; if it can't recover, returns the input unchanged (import then rejects it).
|
|
11
|
+
*/
|
|
12
|
+
export declare function recover_ogp_bytes(bytes: Uint8Array): Uint8Array;
|
|
13
|
+
/** Compress + encrypt a dump object into `.ogp` bytes. Async — never blocks the event loop. */
|
|
14
|
+
export declare function ogp_encode(dump: unknown, secret: string | undefined): Promise<Uint8Array>;
|
|
15
|
+
/**
|
|
16
|
+
* Decrypt + gunzip `.ogp` bytes back to the dump object. Throws on a wrong key or a tampered file
|
|
17
|
+
* (the GCM tag fails to verify) — the caller turns that into a "wrong key" message.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ogp_decode(bytes: Uint8Array, secret: string | undefined): Promise<unknown>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/profiler/crypto.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `.ogp` codec — a profiler dump Brotli-compressed, then AES-256-GCM encrypted with the key.
|
|
4
|
+
* A downloaded trace exposes server internals (source paths, component names, outbound URLs), so the
|
|
5
|
+
* file can only be reopened by a profiler holding the same key. Node-only (dynamic `node:crypto` +
|
|
6
|
+
* `node:zlib`), kept out of the edge-safe report renderer.
|
|
7
|
+
*
|
|
8
|
+
* Self-describing layout: 'OGP1' (4) · salt (16) · iv (12) · ciphertext · GCM tag (16)
|
|
9
|
+
*
|
|
10
|
+
* The AES key is `scrypt(secret, salt, 32)` — a fresh salt per file. `secret` is the profiler auth
|
|
11
|
+
* secret; with none (dev) a fixed dev key is used so dev exports still open in dev.
|
|
12
|
+
*/
|
|
13
|
+
const MAGIC = [
|
|
14
|
+
79,
|
|
15
|
+
71,
|
|
16
|
+
80,
|
|
17
|
+
49
|
|
18
|
+
];
|
|
19
|
+
const DEV_KEY = "ogygia-profiler-dev-key";
|
|
20
|
+
async function node() {
|
|
21
|
+
const [crypto, zlib, util] = await Promise.all([
|
|
22
|
+
import("node:crypto"),
|
|
23
|
+
import("node:zlib"),
|
|
24
|
+
import("node:util")
|
|
25
|
+
]);
|
|
26
|
+
const p = util.promisify;
|
|
27
|
+
return {
|
|
28
|
+
crypto,
|
|
29
|
+
zlib,
|
|
30
|
+
scrypt: p(crypto.scrypt),
|
|
31
|
+
brotli: p(zlib.brotliCompress),
|
|
32
|
+
unbrotli: p(zlib.brotliDecompress)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** True when `bytes` starts with the `.ogp` magic (so import can tell it from legacy plain JSON). */
|
|
36
|
+
function is_ogp(bytes) {
|
|
37
|
+
return bytes.length > 44 && bytes[0] === MAGIC[0] && bytes[1] === MAGIC[1] && bytes[2] === MAGIC[2] && bytes[3] === MAGIC[3];
|
|
38
|
+
}
|
|
39
|
+
const BASE64_RE = /^[A-Za-z0-9+/=\s]+$/;
|
|
40
|
+
/**
|
|
41
|
+
* Undo the two ways a `.ogp` gets mangled moving BETWEEN machines through a text-y channel (a shared
|
|
42
|
+
* clipboard, an editor re-save, a chat) — the file arrives byte-perfect on either side of the codec,
|
|
43
|
+
* so the raw magic is intact, but its wrapper isn't:
|
|
44
|
+
* - a **UTF-8 BOM** prepended (`EF BB BF`) — strip it;
|
|
45
|
+
* - the whole file **base64-encoded** — decode it.
|
|
46
|
+
* A real `.ogp` already starts with the magic, so this is a no-op on it. Returns bytes to hand to
|
|
47
|
+
* `is_ogp` / `ogp_decode`; if it can't recover, returns the input unchanged (import then rejects it).
|
|
48
|
+
*/
|
|
49
|
+
function recover_ogp_bytes(bytes) {
|
|
50
|
+
if (is_ogp(bytes)) return bytes;
|
|
51
|
+
if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
|
|
52
|
+
const stripped = bytes.subarray(3);
|
|
53
|
+
if (is_ogp(stripped)) return stripped;
|
|
54
|
+
bytes = stripped;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes).trim();
|
|
58
|
+
if (text.length > 8 && BASE64_RE.test(text)) {
|
|
59
|
+
const decoded = new Uint8Array(Buffer.from(text, "base64"));
|
|
60
|
+
if (is_ogp(decoded)) return decoded;
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
return bytes;
|
|
64
|
+
}
|
|
65
|
+
/** Compress + encrypt a dump object into `.ogp` bytes. Async — never blocks the event loop. */
|
|
66
|
+
async function ogp_encode(dump, secret) {
|
|
67
|
+
const { crypto, zlib, scrypt, brotli } = await node();
|
|
68
|
+
const gz = await brotli(Buffer.from(JSON.stringify(dump), "utf8"), { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 5 } });
|
|
69
|
+
const salt = crypto.randomBytes(16);
|
|
70
|
+
const iv = crypto.randomBytes(12);
|
|
71
|
+
const key = await scrypt(secret || DEV_KEY, salt, 32);
|
|
72
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
73
|
+
const ct = Buffer.concat([cipher.update(gz), cipher.final()]);
|
|
74
|
+
const tag = cipher.getAuthTag();
|
|
75
|
+
return Buffer.concat([
|
|
76
|
+
Buffer.from(MAGIC),
|
|
77
|
+
salt,
|
|
78
|
+
iv,
|
|
79
|
+
ct,
|
|
80
|
+
tag
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decrypt + gunzip `.ogp` bytes back to the dump object. Throws on a wrong key or a tampered file
|
|
85
|
+
* (the GCM tag fails to verify) — the caller turns that into a "wrong key" message.
|
|
86
|
+
*/
|
|
87
|
+
async function ogp_decode(bytes, secret) {
|
|
88
|
+
const { crypto, scrypt, unbrotli } = await node();
|
|
89
|
+
const buf = Buffer.from(bytes);
|
|
90
|
+
if (!is_ogp(buf)) throw new Error("not an .ogp file");
|
|
91
|
+
const salt = buf.subarray(4, 20);
|
|
92
|
+
const iv = buf.subarray(20, 32);
|
|
93
|
+
const tag = buf.subarray(buf.length - 16);
|
|
94
|
+
const ct = buf.subarray(32, buf.length - 16);
|
|
95
|
+
const key = await scrypt(secret || DEV_KEY, salt, 32);
|
|
96
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
97
|
+
decipher.setAuthTag(tag);
|
|
98
|
+
const gz = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
99
|
+
return JSON.parse((await unbrotli(gz)).toString("utf8"));
|
|
100
|
+
}
|
|
101
|
+
export { is_ogp, ogp_decode, ogp_encode, recover_ogp_bytes };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT CHANGES BETWEEN TWO RENDERS OF ONE PAGE: both documents split at their tags, lined up, and
|
|
3
|
+
* every piece that differs grouped by what it is — an island's props (its region and its props
|
|
4
|
+
* script change together), one attribute on one kind of tag (a random id stamped per render), or
|
|
5
|
+
* the text inside a tag. A page the same but for a few such parts is a page to cache whole once
|
|
6
|
+
* those parts stop changing. Hand-parsed (indexOf), no regex: a document can be megabytes.
|
|
7
|
+
*/
|
|
8
|
+
export interface DocDiffGroup {
|
|
9
|
+
/** what changes, for a reader: `data-track on <a>`, `the Price island's props`, `text in <p>` */
|
|
10
|
+
what: string;
|
|
11
|
+
tag: string;
|
|
12
|
+
attr?: string;
|
|
13
|
+
island?: string;
|
|
14
|
+
/** places it differs */
|
|
15
|
+
count: number;
|
|
16
|
+
/** one example, first render against the last */
|
|
17
|
+
a: string;
|
|
18
|
+
b: string;
|
|
19
|
+
/** where your markup sets it (an attribute given a value from code), when a component file does */
|
|
20
|
+
sources?: {
|
|
21
|
+
file: string;
|
|
22
|
+
line: number;
|
|
23
|
+
code: string;
|
|
24
|
+
}[];
|
|
25
|
+
/** an attribute no file of yours sets: a library stamps it as it renders */
|
|
26
|
+
library?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** The lines of a component's markup that give `attr` a value from code on one of `tags`
|
|
29
|
+
* (`<a … data-track={id}>`): plain text search, the attribute's name as written. */
|
|
30
|
+
export declare function attr_sites(src: string, attr: string, tags: readonly string[]): {
|
|
31
|
+
line: number;
|
|
32
|
+
code: string;
|
|
33
|
+
}[];
|
|
34
|
+
export interface DocDiff {
|
|
35
|
+
/** tag-delimited pieces of the later document */
|
|
36
|
+
tokens: number;
|
|
37
|
+
/** pieces that differ */
|
|
38
|
+
differing: number;
|
|
39
|
+
/** the documents lined up piece by piece (same count); else matched past small insertions */
|
|
40
|
+
aligned: boolean;
|
|
41
|
+
groups: DocDiffGroup[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The two documents' differences, grouped. `island_of(entry)` names an island by its entry URL (the
|
|
45
|
+
* report's island rows). Undefined when they are the same, or differ too much to line up.
|
|
46
|
+
*/
|
|
47
|
+
export declare function doc_diff(a: string, b: string, island_of?: (entry: string) => string | undefined, max_groups?: number): DocDiff | undefined;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// src/profiler/doc-diff.ts
|
|
2
|
+
/** The lines of a component's markup that give `attr` a value from code on one of `tags`
|
|
3
|
+
* (`<a … data-track={id}>`): plain text search, the attribute's name as written. */
|
|
4
|
+
function attr_sites(src, attr, tags) {
|
|
5
|
+
const out = [];
|
|
6
|
+
const lines = src.split("\n");
|
|
7
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8
|
+
const l = lines[i];
|
|
9
|
+
let at = -1;
|
|
10
|
+
for (const form of [
|
|
11
|
+
`${attr}={`,
|
|
12
|
+
`${attr}="{`,
|
|
13
|
+
...attr.includes("-") ? [] : [`{${attr}}`]
|
|
14
|
+
]) {
|
|
15
|
+
let p = l.indexOf(form);
|
|
16
|
+
while (p !== -1 && p > 0 && l[p - 1] !== " " && l[p - 1] !== " ") p = l.indexOf(form, p + 1);
|
|
17
|
+
if (p !== -1 && (at === -1 || p < at)) at = p;
|
|
18
|
+
}
|
|
19
|
+
if (at === -1) continue;
|
|
20
|
+
if (!tags.filter((t) => {
|
|
21
|
+
let o = l.indexOf(`<${t}`);
|
|
22
|
+
while (o !== -1 && o < at) {
|
|
23
|
+
const next = l[o + t.length + 1];
|
|
24
|
+
if (next === void 0 || next === " " || next === ">" || next === "/" || next === " ") return true;
|
|
25
|
+
o = l.indexOf(`<${t}`, o + 1);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}).length && l.slice(0, at).includes("<")) continue;
|
|
29
|
+
out.push({
|
|
30
|
+
line: i + 1,
|
|
31
|
+
code: l.trim().length > 140 ? l.trim().slice(0, 139) + "…" : l.trim()
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/** a document at its tags: each piece starts at a `<` */
|
|
37
|
+
function pieces(doc) {
|
|
38
|
+
const out = [];
|
|
39
|
+
let at = 0;
|
|
40
|
+
while (at < doc.length) {
|
|
41
|
+
const next = doc.indexOf("<", at + 1);
|
|
42
|
+
const end = next === -1 ? doc.length : next;
|
|
43
|
+
out.push(doc.slice(at, end));
|
|
44
|
+
at = end;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/** `<a href="x" data-track="1">text` → tag `a`, attributes, the text after the tag */
|
|
49
|
+
function parse(piece) {
|
|
50
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
51
|
+
if (piece[0] !== "<") return {
|
|
52
|
+
tag: "text",
|
|
53
|
+
attrs,
|
|
54
|
+
text: piece
|
|
55
|
+
};
|
|
56
|
+
let i = 1;
|
|
57
|
+
if (piece[i] === "/") i++;
|
|
58
|
+
let j = i;
|
|
59
|
+
while (j < piece.length && piece[j] !== " " && piece[j] !== ">" && piece[j] !== "/" && piece[j] !== "\n" && piece[j] !== " ") j++;
|
|
60
|
+
const tag = piece.slice(i, j).toLowerCase();
|
|
61
|
+
const close = piece.indexOf(">", j);
|
|
62
|
+
const head = close === -1 ? piece.slice(j) : piece.slice(j, close);
|
|
63
|
+
let k = 0;
|
|
64
|
+
while (k < head.length) {
|
|
65
|
+
let eq = head.indexOf("=", k);
|
|
66
|
+
while (eq !== -1 && head[eq + 1] !== "\"" && head[eq + 1] !== "'") eq = head.indexOf("=", eq + 1);
|
|
67
|
+
if (eq === -1) break;
|
|
68
|
+
const q = head[eq + 1];
|
|
69
|
+
let s = eq - 1;
|
|
70
|
+
while (s >= 0 && head[s] !== " " && head[s] !== "\n" && head[s] !== " ") s--;
|
|
71
|
+
const name = head.slice(s + 1, eq);
|
|
72
|
+
const endq = head.indexOf(q, eq + 2);
|
|
73
|
+
if (endq === -1) break;
|
|
74
|
+
attrs.set(name, head.slice(eq + 2, endq));
|
|
75
|
+
k = endq + 1;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
tag,
|
|
79
|
+
attrs,
|
|
80
|
+
text: close === -1 ? "" : piece.slice(close + 1)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const clip = (s) => s.length > 60 ? s.slice(0, 59) + "…" : s;
|
|
84
|
+
/**
|
|
85
|
+
* The two documents' differences, grouped. `island_of(entry)` names an island by its entry URL (the
|
|
86
|
+
* report's island rows). Undefined when they are the same, or differ too much to line up.
|
|
87
|
+
*/
|
|
88
|
+
function doc_diff(a, b, island_of = () => void 0, max_groups = 8) {
|
|
89
|
+
if (a === b) return void 0;
|
|
90
|
+
const pa = pieces(a);
|
|
91
|
+
const pb = pieces(b);
|
|
92
|
+
const aligned = pa.length === pb.length;
|
|
93
|
+
const entry_of_fp = /* @__PURE__ */ new Map();
|
|
94
|
+
const pairs = [];
|
|
95
|
+
if (aligned) {
|
|
96
|
+
for (let i = 0; i < pb.length; i++) if (pa[i] !== pb[i]) pairs.push([pa[i], pb[i]]);
|
|
97
|
+
} else {
|
|
98
|
+
let i = 0;
|
|
99
|
+
let j = 0;
|
|
100
|
+
const AHEAD = 24;
|
|
101
|
+
while (i < pa.length && j < pb.length) {
|
|
102
|
+
if (pa[i] === pb[j]) {
|
|
103
|
+
i++;
|
|
104
|
+
j++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
let moved = false;
|
|
108
|
+
for (let d = 1; d <= AHEAD && !moved; d++) if (j + d < pb.length && pa[i] === pb[j + d]) {
|
|
109
|
+
for (let x = 0; x < d; x++) pairs.push(["", pb[j + x]]);
|
|
110
|
+
j += d;
|
|
111
|
+
moved = true;
|
|
112
|
+
} else if (i + d < pa.length && pa[i + d] === pb[j]) {
|
|
113
|
+
for (let x = 0; x < d; x++) pairs.push([pa[i + x], ""]);
|
|
114
|
+
i += d;
|
|
115
|
+
moved = true;
|
|
116
|
+
}
|
|
117
|
+
if (!moved) {
|
|
118
|
+
pairs.push([pa[i], pb[j]]);
|
|
119
|
+
i++;
|
|
120
|
+
j++;
|
|
121
|
+
}
|
|
122
|
+
if (pairs.length > 50 && pairs.length > pb.length * .25) return void 0;
|
|
123
|
+
}
|
|
124
|
+
for (; i < pa.length; i++) pairs.push([pa[i], ""]);
|
|
125
|
+
for (; j < pb.length; j++) pairs.push(["", pb[j]]);
|
|
126
|
+
if (pairs.length > 50 && pairs.length > pb.length * .25) return void 0;
|
|
127
|
+
}
|
|
128
|
+
for (const p of pb) {
|
|
129
|
+
if (!p.startsWith("<ogygia-region")) continue;
|
|
130
|
+
const { attrs } = parse(p);
|
|
131
|
+
const fp = attrs.get("data-og-fp");
|
|
132
|
+
const entry = attrs.get("entry");
|
|
133
|
+
if (fp && entry) entry_of_fp.set(fp, entry);
|
|
134
|
+
}
|
|
135
|
+
const groups = /* @__PURE__ */ new Map();
|
|
136
|
+
const add = (key, g) => {
|
|
137
|
+
const cur = groups.get(key);
|
|
138
|
+
if (cur) cur.count++;
|
|
139
|
+
else groups.set(key, {
|
|
140
|
+
...g,
|
|
141
|
+
count: 1
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
for (const [x, y] of pairs) {
|
|
145
|
+
if (!x || !y) {
|
|
146
|
+
const only = parse(x || y);
|
|
147
|
+
const verb = x ? "dropped" : "added";
|
|
148
|
+
add(`${verb}\0${only.tag}`, {
|
|
149
|
+
what: `<${only.tag}> ${verb} in the later render`,
|
|
150
|
+
tag: only.tag,
|
|
151
|
+
a: clip(x.slice(0, 60)),
|
|
152
|
+
b: clip(y.slice(0, 60))
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (y.startsWith("<!--")) {
|
|
157
|
+
const end = y.indexOf("-->");
|
|
158
|
+
add("comment", {
|
|
159
|
+
what: "comment markers (<!--…-->)",
|
|
160
|
+
tag: "comment",
|
|
161
|
+
a: clip(x.slice(0, x.indexOf("-->") + 3)),
|
|
162
|
+
b: clip(end === -1 ? y : y.slice(0, end + 3))
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const py = parse(y);
|
|
167
|
+
const px = parse(x);
|
|
168
|
+
if (py.tag === "ogygia-region" || py.tag === "script" && py.attrs.has("data-ogygia-props")) {
|
|
169
|
+
const entry = py.tag === "ogygia-region" ? py.attrs.get("entry") : entry_of_fp.get(py.attrs.get("data-ogygia-props") ?? "");
|
|
170
|
+
const name = entry && island_of(entry) || "an";
|
|
171
|
+
if (py.tag === "script") continue;
|
|
172
|
+
add(`island\0${name}`, {
|
|
173
|
+
what: `the ${name} island's props`,
|
|
174
|
+
tag: py.tag,
|
|
175
|
+
island: name,
|
|
176
|
+
a: clip(px.attrs.get("data-og-fp") ?? ""),
|
|
177
|
+
b: clip(py.attrs.get("data-og-fp") ?? "")
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const changed = [...py.attrs].filter(([k, v]) => px.attrs.get(k) !== v).map(([k]) => k);
|
|
182
|
+
for (const k of px.attrs.keys()) if (!py.attrs.has(k)) changed.push(k);
|
|
183
|
+
if (changed.length) for (const attr of changed) {
|
|
184
|
+
const key = `attr\0${attr}`;
|
|
185
|
+
const g = groups.get(key);
|
|
186
|
+
if (g && !g.tag.split(", ").includes(py.tag)) g.tag += `, ${py.tag}`;
|
|
187
|
+
add(key, {
|
|
188
|
+
what: "",
|
|
189
|
+
tag: py.tag,
|
|
190
|
+
attr,
|
|
191
|
+
a: clip(px.attrs.get(attr) ?? ""),
|
|
192
|
+
b: clip(py.attrs.get(attr) ?? "")
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else add(`text\0${py.tag}`, {
|
|
196
|
+
what: `text in <${py.tag}>`,
|
|
197
|
+
tag: py.tag,
|
|
198
|
+
a: clip(px.text.trim()),
|
|
199
|
+
b: clip(py.text.trim())
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
for (const g of groups.values()) {
|
|
203
|
+
if (!g.attr) continue;
|
|
204
|
+
const tags = g.tag.split(", ");
|
|
205
|
+
g.what = `${g.attr} on ${tags.slice(0, 3).map((t) => `<${t}>`).join(", ")}${tags.length > 3 ? ` and ${tags.length - 3} more` : ""}`;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
tokens: pb.length,
|
|
209
|
+
differing: pairs.length,
|
|
210
|
+
aligned,
|
|
211
|
+
groups: [...groups.values()].sort((m, n) => n.count - m.count).slice(0, max_groups)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
export { attr_sites, doc_diff };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM TIME TRAVEL — an island's markup at three moments (as the server sent it, after it hydrated,
|
|
3
|
+
* when the page went away) and what changed between them, plus the layout shifts of a visit
|
|
4
|
+
* drawn as boxes. Pure: `html_diff` is a token diff (tags and text runs) with a size cap so a huge
|
|
5
|
+
* island never freezes the report; `shift_boxes` normalizes shift rectangles to the viewport.
|
|
6
|
+
*/
|
|
7
|
+
export interface DiffOp {
|
|
8
|
+
kind: "same" | "add" | "del";
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
export interface HtmlDiff {
|
|
12
|
+
ops: DiffOp[];
|
|
13
|
+
added: number;
|
|
14
|
+
removed: number;
|
|
15
|
+
/** the middle was too large to diff token by token and is shown as one removal + one addition */
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
/** the tokens both sides share, as a share of the longer side (1 = identical) */
|
|
18
|
+
same_ratio: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function tokenize_html(s: string): string[];
|
|
21
|
+
export declare function html_diff(a: string, b: string): HtmlDiff;
|
|
22
|
+
export interface ShiftBox {
|
|
23
|
+
/** 0..1 of the viewport */
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
w: number;
|
|
27
|
+
h: number;
|
|
28
|
+
/** where it came from, same units */
|
|
29
|
+
fx: number;
|
|
30
|
+
fy: number;
|
|
31
|
+
value: number;
|
|
32
|
+
t: number;
|
|
33
|
+
fp?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Layout-shift rectangles normalized to the viewport, biggest shift first; boxes outside the
|
|
36
|
+
* viewport are clipped, a missing rect is skipped. */
|
|
37
|
+
export declare function shift_boxes(shifts: {
|
|
38
|
+
t: number;
|
|
39
|
+
value: number;
|
|
40
|
+
fp?: string;
|
|
41
|
+
from?: [number, number, number, number];
|
|
42
|
+
to?: [number, number, number, number];
|
|
43
|
+
}[], viewport: [number, number] | undefined): ShiftBox[];
|