@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,733 @@
|
|
|
1
|
+
import { own_requests } from "./own-requests.js";
|
|
2
|
+
import { at_line } from "./drill.js";
|
|
3
|
+
import { saving_of } from "./saving.js";
|
|
4
|
+
import { PHASE_LABEL } from "./timeline.js";
|
|
5
|
+
import { vitals_moved } from "@ogygia/core/insights";
|
|
6
|
+
// src/profiler/compare.ts
|
|
7
|
+
/** The render's own noise decides: both reports' runs, the median moved no more than their spread
|
|
8
|
+
* (or 3 %). Then the CPU rows' moves are attribution, not change. */
|
|
9
|
+
function render_same_of(a, b) {
|
|
10
|
+
const ra = a.runs ?? [];
|
|
11
|
+
const rb = b.runs ?? [];
|
|
12
|
+
if (ra.length < 2 || rb.length < 2) return {};
|
|
13
|
+
const spread = (r) => Math.max(...r) - Math.min(...r);
|
|
14
|
+
const ma = median(ra);
|
|
15
|
+
const mb = median(rb);
|
|
16
|
+
const noise = Math.max(spread(ra), spread(rb), .03 * Math.max(ma, mb));
|
|
17
|
+
return Math.abs(mb - ma) <= noise ? { render_same: {
|
|
18
|
+
a_ms: round2(ma),
|
|
19
|
+
b_ms: round2(mb),
|
|
20
|
+
noise_ms: round2(noise)
|
|
21
|
+
} } : {};
|
|
22
|
+
}
|
|
23
|
+
/** With the render unchanged, a CPU row's fixed / new / better / worse is where V8 charged the time. */
|
|
24
|
+
function mark_shifted(cmp) {
|
|
25
|
+
if (!cmp.render_same) return cmp;
|
|
26
|
+
for (const p of cmp.patterns) if (p.status !== "same" && !p.wait && p.kind !== "kept-per-render" && p.kind !== "seed-whole-read") p.status = "shifted";
|
|
27
|
+
for (const l of cmp.lines) if (l.status !== "same" && Math.abs(l.b_kept - l.a_kept) < 131072) l.status = "shifted";
|
|
28
|
+
for (const r of cmp.drill ?? []) if ((r.kind === "cpu" || r.kind === "line") && !r.waiting) r.status = "shifted";
|
|
29
|
+
if (cmp.drill) cmp.drill.sort((x, y) => Number(x.status === "shifted") - Number(y.status === "shifted") || Math.abs(y.d_ms) - Math.abs(x.d_ms));
|
|
30
|
+
return cmp;
|
|
31
|
+
}
|
|
32
|
+
const renders_of = (m) => m.trigger === "page" ? Math.max(1, m.runs?.length ?? 1) : 1;
|
|
33
|
+
/** The two ledgers side by side. Keyed by file + the code on the line: code that changed reads as
|
|
34
|
+
* one line gone and another new (the fix), code that did not keeps its row though its number moved. */
|
|
35
|
+
function line_deltas(a = [], b = [], ra = 1, rb = 1) {
|
|
36
|
+
const key = (l) => l.file + "\0" + (l.code ?? `#${l.line}`);
|
|
37
|
+
const cost = (l, r) => ({
|
|
38
|
+
ms: round2((l.cpu_ms + l.lib_ms) / r),
|
|
39
|
+
bytes: Math.round(l.alloc_bytes / r),
|
|
40
|
+
kept: l.retained_bytes
|
|
41
|
+
});
|
|
42
|
+
const out = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const l of a) {
|
|
44
|
+
const c = cost(l, ra);
|
|
45
|
+
out.set(key(l), {
|
|
46
|
+
file: l.file,
|
|
47
|
+
line: l.line,
|
|
48
|
+
code: l.code ?? "",
|
|
49
|
+
a_ms: c.ms,
|
|
50
|
+
b_ms: 0,
|
|
51
|
+
d_ms: -c.ms,
|
|
52
|
+
a_bytes: c.bytes,
|
|
53
|
+
b_bytes: 0,
|
|
54
|
+
a_kept: c.kept,
|
|
55
|
+
b_kept: 0,
|
|
56
|
+
status: "fixed"
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
for (const l of b) {
|
|
60
|
+
const c = cost(l, rb);
|
|
61
|
+
const r = out.get(key(l));
|
|
62
|
+
if (!r) {
|
|
63
|
+
out.set(key(l), {
|
|
64
|
+
file: l.file,
|
|
65
|
+
line: l.line,
|
|
66
|
+
code: l.code ?? "",
|
|
67
|
+
a_ms: 0,
|
|
68
|
+
b_ms: c.ms,
|
|
69
|
+
d_ms: c.ms,
|
|
70
|
+
a_bytes: 0,
|
|
71
|
+
b_bytes: c.bytes,
|
|
72
|
+
a_kept: 0,
|
|
73
|
+
b_kept: c.kept,
|
|
74
|
+
status: "new"
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
Object.assign(r, {
|
|
79
|
+
line: l.line,
|
|
80
|
+
b_ms: c.ms,
|
|
81
|
+
d_ms: round2(c.ms - r.a_ms),
|
|
82
|
+
b_bytes: c.bytes,
|
|
83
|
+
b_kept: c.kept
|
|
84
|
+
});
|
|
85
|
+
const t = Math.abs(r.d_ms) >= Math.max(.2 * r.a_ms, 1);
|
|
86
|
+
const m = Math.abs(c.bytes - r.a_bytes) >= .5 * Math.max(r.a_bytes, 1) && Math.abs(c.bytes - r.a_bytes) >= 262144;
|
|
87
|
+
const k = Math.abs(c.kept - r.a_kept) >= .5 * Math.max(r.a_kept, 1) && Math.abs(c.kept - r.a_kept) >= 131072;
|
|
88
|
+
const worse = r.d_ms > 0 || c.bytes > r.a_bytes || c.kept > r.a_kept;
|
|
89
|
+
r.status = !(t || m || k) ? "same" : worse && !(r.d_ms < 0 && t) ? "worse" : "better";
|
|
90
|
+
}
|
|
91
|
+
const rank = {
|
|
92
|
+
fixed: 0,
|
|
93
|
+
new: 1,
|
|
94
|
+
worse: 2,
|
|
95
|
+
better: 3,
|
|
96
|
+
shifted: 4,
|
|
97
|
+
same: 5
|
|
98
|
+
};
|
|
99
|
+
const costly = (ms, bytes, kept) => ms >= 1 || bytes >= 1048576 || kept >= 262144;
|
|
100
|
+
return [...out.values()].filter((r) => (r.status !== "fixed" || costly(r.a_ms, r.a_bytes, r.a_kept)) && (r.status !== "new" || costly(r.b_ms, r.b_bytes, r.b_kept))).sort((x, y) => rank[x.status] - rank[y.status] || Math.abs(y.d_ms) - Math.abs(x.d_ms)).slice(0, 40);
|
|
101
|
+
}
|
|
102
|
+
function island_files_diff(a, b, weight) {
|
|
103
|
+
const before = new Map(a.map((r) => [r.entry, r.module_url]));
|
|
104
|
+
const moved = [];
|
|
105
|
+
let kept = 0;
|
|
106
|
+
const seen = /* @__PURE__ */ new Set();
|
|
107
|
+
for (const r of b) {
|
|
108
|
+
if (seen.has(r.entry)) continue;
|
|
109
|
+
seen.add(r.entry);
|
|
110
|
+
const was = before.get(r.entry);
|
|
111
|
+
if (was === void 0 || was === r.entry || r.module_url === r.entry) continue;
|
|
112
|
+
if (was === r.module_url) kept++;
|
|
113
|
+
else moved.push({
|
|
114
|
+
name: r.name,
|
|
115
|
+
bytes: weight?.(r.module_url) ?? null
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (!moved.length && !kept) return null;
|
|
119
|
+
return {
|
|
120
|
+
moved,
|
|
121
|
+
kept,
|
|
122
|
+
bytes: moved.reduce((s, m) => s + (m.bytes ?? 0), 0),
|
|
123
|
+
all_moved: moved.length >= 3 && kept === 0
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** memory kept per render that slows the renders after it on the same server */
|
|
127
|
+
const ORDER_KEPT_BYTES = 8388608;
|
|
128
|
+
/** One server for both (its start, from each report's time and the server's age then, within the
|
|
129
|
+
* length of a profile; and it had served more requests by the later one), and the earlier report
|
|
130
|
+
* saw each render keep 8 MB+ alive: the later one inherited the fuller heap. */
|
|
131
|
+
function order_of(a, b) {
|
|
132
|
+
const ia = a.meta.instance;
|
|
133
|
+
const ib = b.meta.instance;
|
|
134
|
+
if (!ia || !ib) return void 0;
|
|
135
|
+
const start = (m, age_s) => m.created - age_s * 1e3;
|
|
136
|
+
if (Math.abs(start(a.meta, ia.age_s) - start(b.meta, ib.age_s)) > 3e4) return void 0;
|
|
137
|
+
const [first, second, earlier] = a.meta.created <= b.meta.created ? [
|
|
138
|
+
a,
|
|
139
|
+
b,
|
|
140
|
+
"a"
|
|
141
|
+
] : [
|
|
142
|
+
b,
|
|
143
|
+
a,
|
|
144
|
+
"b"
|
|
145
|
+
];
|
|
146
|
+
const between = (second.meta.instance.requests_before ?? 0) - (first.meta.instance.requests_before ?? 0);
|
|
147
|
+
if (between <= 0) return void 0;
|
|
148
|
+
const kept = first.patterns?.find((p) => p.kind === "kept-per-render")?.kept_bytes ?? 0;
|
|
149
|
+
if (kept < ORDER_KEPT_BYTES) return void 0;
|
|
150
|
+
return {
|
|
151
|
+
earlier,
|
|
152
|
+
kept_mb: Math.round(kept / 1048576 * 10) / 10,
|
|
153
|
+
requests_between: between
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** a move worth naming: 2 ms and a fifth of the row (one render per side, so small moves are noise) */
|
|
157
|
+
const drill_moved = (a, b) => Math.abs(b - a) >= Math.max(2, .2 * Math.max(a, b));
|
|
158
|
+
/**
|
|
159
|
+
* THE DRILL-DOWNS SIDE BY SIDE: every row of one render's tree matched by its place (phase › load
|
|
160
|
+
* file › who › line), and the rows that moved — only the DEEPEST of a chain: a row whose moved
|
|
161
|
+
* children carry most (70 %) of its move is left out, so the list names the cause (the call, the
|
|
162
|
+
* line), not every level above it. Both trees are one render, so no per-render division.
|
|
163
|
+
*/
|
|
164
|
+
function drill_deltas(a, b) {
|
|
165
|
+
if (!a || !b) return [];
|
|
166
|
+
const rows = /* @__PURE__ */ new Map();
|
|
167
|
+
const walk = (n, path, side, parent) => {
|
|
168
|
+
const k = path.join("\0");
|
|
169
|
+
const r = rows.get(k) ?? rows.set(k, {
|
|
170
|
+
path,
|
|
171
|
+
kind: n.kind,
|
|
172
|
+
...n.at ? { at: n.at } : {},
|
|
173
|
+
a: 0,
|
|
174
|
+
b: 0,
|
|
175
|
+
kids: [],
|
|
176
|
+
...parent ? { parent } : {},
|
|
177
|
+
folded: {
|
|
178
|
+
a: 0,
|
|
179
|
+
b: 0
|
|
180
|
+
}
|
|
181
|
+
}).get(k);
|
|
182
|
+
r[side] += n.ms;
|
|
183
|
+
if (n.runs) r[side === "a" ? "runs_a" : "runs_b"] = n.runs;
|
|
184
|
+
if (n.code) r[side === "a" ? "code_a" : "code_b"] = n.code;
|
|
185
|
+
if (n.kind === "wait" || n.kind === "gap" || (parent ? rows.get(parent)?.waiting : false)) r.waiting = true;
|
|
186
|
+
if (n.at) r.at = n.at;
|
|
187
|
+
for (const c of n.children ?? []) {
|
|
188
|
+
if (c.kind === "more") {
|
|
189
|
+
if (!c.children?.length) r.folded[side] = c.ms;
|
|
190
|
+
for (const f of c.children ?? []) {
|
|
191
|
+
const fk = walk(f, [...path, f.label], side, k);
|
|
192
|
+
if (!r.kids.includes(fk)) r.kids.push(fk);
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const ck = walk(c, [...path, c.label], side, k);
|
|
197
|
+
if (!r.kids.includes(ck)) r.kids.push(ck);
|
|
198
|
+
}
|
|
199
|
+
return k;
|
|
200
|
+
};
|
|
201
|
+
const top = {
|
|
202
|
+
path: [],
|
|
203
|
+
kind: "render",
|
|
204
|
+
a: 0,
|
|
205
|
+
b: 0,
|
|
206
|
+
kids: [],
|
|
207
|
+
folded: {
|
|
208
|
+
a: 0,
|
|
209
|
+
b: 0
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
rows.set("", top);
|
|
213
|
+
for (const [side, root] of [["a", a], ["b", b]]) for (const c of root.children ?? []) if (c.kind !== "more") walk(c, [c.label], side, "");
|
|
214
|
+
else if (!c.children?.length) top.folded[side] = c.ms;
|
|
215
|
+
else for (const f of c.children) walk(f, [f.label], side, "");
|
|
216
|
+
rows.delete("");
|
|
217
|
+
const into = /* @__PURE__ */ new Map();
|
|
218
|
+
const lines = [...rows.entries()].filter(([, r]) => r.kind === "line").sort(([, x], [, y]) => x.path.length - y.path.length);
|
|
219
|
+
const same_parent = (x, y) => (into.get(x.parent ?? "") ?? x.parent) === y.parent;
|
|
220
|
+
const paired = /* @__PURE__ */ new Set();
|
|
221
|
+
for (const [fk, f] of lines) {
|
|
222
|
+
if (f.a < .05 || f.b >= .05) continue;
|
|
223
|
+
const fl = f.at ? at_line(f.at) : void 0;
|
|
224
|
+
if (!fl) continue;
|
|
225
|
+
let best;
|
|
226
|
+
let best_d = Infinity;
|
|
227
|
+
for (const [nk, n] of lines) {
|
|
228
|
+
if (n.a >= .05 || n.b < .05 || paired.has(nk) || !same_parent(f, n)) continue;
|
|
229
|
+
const nl = n.at ? at_line(n.at) : void 0;
|
|
230
|
+
if (!nl || nl.file !== fl.file) continue;
|
|
231
|
+
const dist = Math.abs(nl.line - fl.line);
|
|
232
|
+
if (dist > 40 || dist >= best_d) continue;
|
|
233
|
+
if (f.code_a && n.code_b ? f.code_a !== n.code_b : Math.abs(n.b - f.a) > Math.max(2, .2 * f.a)) continue;
|
|
234
|
+
best = nk;
|
|
235
|
+
best_d = dist;
|
|
236
|
+
}
|
|
237
|
+
if (!best) continue;
|
|
238
|
+
const n = rows.get(best);
|
|
239
|
+
paired.add(best);
|
|
240
|
+
into.set(fk, best);
|
|
241
|
+
n.a = f.a;
|
|
242
|
+
if (f.runs_a) n.runs_a = f.runs_a;
|
|
243
|
+
}
|
|
244
|
+
for (const [k, r] of rows) {
|
|
245
|
+
if (into.has(k)) continue;
|
|
246
|
+
for (const side of ["a", "b"]) {
|
|
247
|
+
if (r[side] >= .05) continue;
|
|
248
|
+
const fold = (r.parent === void 0 ? top : rows.get(r.parent))?.folded[side] ?? 0;
|
|
249
|
+
if (fold <= 0) continue;
|
|
250
|
+
r[side] = Math.min(fold, side === "a" ? r.b : r.a);
|
|
251
|
+
r.upto = side;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const moved = (r) => {
|
|
255
|
+
if (r.runs_a && r.runs_b) {
|
|
256
|
+
const [a0, a1] = r.runs_a;
|
|
257
|
+
const [b0, b1] = r.runs_b;
|
|
258
|
+
return (a1 < b0 || b1 < a0) && Math.abs(r.b - r.a) >= 2;
|
|
259
|
+
}
|
|
260
|
+
return drill_moved(r.a, r.b);
|
|
261
|
+
};
|
|
262
|
+
const out = [];
|
|
263
|
+
for (const [k, r] of rows) {
|
|
264
|
+
if (into.has(k) || !moved(r)) continue;
|
|
265
|
+
const d = r.b - r.a;
|
|
266
|
+
const kids = r.kids.filter((c) => !into.has(c)).map((c) => rows.get(c)).filter((c) => Math.sign(c.b - c.a) === Math.sign(d));
|
|
267
|
+
const carried = kids.filter((c) => moved(c)).reduce((s, c) => s + (c.b - c.a), 0);
|
|
268
|
+
if (Math.abs(carried) >= .7 * Math.abs(d)) continue;
|
|
269
|
+
const noise = kids.filter((c) => c.runs_a && c.runs_b && !moved(c)).reduce((s, c) => s + (c.b - c.a), 0);
|
|
270
|
+
if (Math.abs(noise) >= .7 * Math.abs(d)) continue;
|
|
271
|
+
out.push({
|
|
272
|
+
path: r.path,
|
|
273
|
+
kind: r.kind,
|
|
274
|
+
...r.at ? { at: r.at } : {},
|
|
275
|
+
a_ms: round2(r.a),
|
|
276
|
+
b_ms: round2(r.b),
|
|
277
|
+
d_ms: round2(d),
|
|
278
|
+
...r.upto ? { upto: r.upto } : {},
|
|
279
|
+
...r.waiting ? { waiting: true } : {},
|
|
280
|
+
status: r.a < .05 ? "new" : r.b < .05 ? "fixed" : d < 0 ? "better" : "worse"
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return out.sort((x, y) => Math.abs(y.d_ms) - Math.abs(x.d_ms)).slice(0, 20);
|
|
284
|
+
}
|
|
285
|
+
/** what makes two reports' patterns the same one: its kind, and for a library call the library */
|
|
286
|
+
const pattern_key = (p) => p.kind + "\0" + (p.kind === "library-per-item" ? p.sites[0]?.libs?.[0] ?? "" : "");
|
|
287
|
+
function pattern_deltas(a = [], b = [], ra = 1, rb = 1) {
|
|
288
|
+
const out = /* @__PURE__ */ new Map();
|
|
289
|
+
const cost = (p, r) => round2(p.wait ? p.cost_ms : p.cost_ms / r);
|
|
290
|
+
const save = (p, r) => round2(p.wait ? p.save_ms : p.save_ms / r);
|
|
291
|
+
for (const p of a) {
|
|
292
|
+
const c = cost(p, ra);
|
|
293
|
+
out.set(pattern_key(p), {
|
|
294
|
+
title: p.title,
|
|
295
|
+
kind: p.kind,
|
|
296
|
+
a_ms: c,
|
|
297
|
+
b_ms: 0,
|
|
298
|
+
d_ms: -c,
|
|
299
|
+
a_save: save(p, ra),
|
|
300
|
+
b_save: 0,
|
|
301
|
+
a_sites: p.sites.length,
|
|
302
|
+
b_sites: 0,
|
|
303
|
+
status: "fixed",
|
|
304
|
+
...p.wait ? { wait: true } : {}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
for (const p of b) {
|
|
308
|
+
const r = out.get(pattern_key(p));
|
|
309
|
+
const c = cost(p, rb);
|
|
310
|
+
if (!r) {
|
|
311
|
+
out.set(pattern_key(p), {
|
|
312
|
+
title: p.title,
|
|
313
|
+
kind: p.kind,
|
|
314
|
+
a_ms: 0,
|
|
315
|
+
b_ms: c,
|
|
316
|
+
d_ms: c,
|
|
317
|
+
a_save: 0,
|
|
318
|
+
b_save: save(p, rb),
|
|
319
|
+
a_sites: 0,
|
|
320
|
+
b_sites: p.sites.length,
|
|
321
|
+
status: "new",
|
|
322
|
+
...p.wait ? { wait: true } : {}
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
r.title = p.title;
|
|
327
|
+
r.b_ms = c;
|
|
328
|
+
r.d_ms = round2(c - r.a_ms);
|
|
329
|
+
r.b_save = save(p, rb);
|
|
330
|
+
r.b_sites = p.sites.length;
|
|
331
|
+
r.status = !(Math.abs(r.d_ms) >= Math.max(.2 * r.a_ms, 2)) ? "same" : r.d_ms < 0 ? "better" : "worse";
|
|
332
|
+
}
|
|
333
|
+
const rank = {
|
|
334
|
+
fixed: 0,
|
|
335
|
+
new: 1,
|
|
336
|
+
worse: 2,
|
|
337
|
+
better: 3,
|
|
338
|
+
shifted: 4,
|
|
339
|
+
same: 5
|
|
340
|
+
};
|
|
341
|
+
for (const r of out.values()) if ((r.status === "fixed" || r.status === "new") && Math.max(r.a_ms, r.b_ms) < 3 && !r.wait) r.status = "same";
|
|
342
|
+
return [...out.values()].sort((x, y) => rank[x.status] - rank[y.status] || Math.abs(y.d_ms) - Math.abs(x.d_ms));
|
|
343
|
+
}
|
|
344
|
+
/** what the fixed and improved patterns promised, without the older report's forecast: not added up
|
|
345
|
+
* blindly — a whole-page cache's "saving" IS the render (it tracks any change), and wait patterns
|
|
346
|
+
* cover the SAME calls (one call made faster shrinks "same answer", "waits in a row" and "one slow
|
|
347
|
+
* call" at once), so the waits promise their largest move, CPU patterns their sum */
|
|
348
|
+
function summed_promise(patterns) {
|
|
349
|
+
let cpu = 0;
|
|
350
|
+
let wait = 0;
|
|
351
|
+
for (const p of patterns) {
|
|
352
|
+
if (p.status !== "fixed" && p.status !== "better") continue;
|
|
353
|
+
if (p.kind === "same-document" || p.kind === "almost-same-document") continue;
|
|
354
|
+
const d = Math.max(0, p.a_save - p.b_save);
|
|
355
|
+
if (p.wait) wait = Math.max(wait, d);
|
|
356
|
+
else cpu += d;
|
|
357
|
+
}
|
|
358
|
+
return cpu + wait;
|
|
359
|
+
}
|
|
360
|
+
/** a title without its "(3 places)": the count changes when a fix shrinks a pattern */
|
|
361
|
+
const bare_title = (t) => {
|
|
362
|
+
const i = t.lastIndexOf(" (");
|
|
363
|
+
return i !== -1 && (t.endsWith(" places)") || t.endsWith(" place)")) ? t.slice(0, i) : t;
|
|
364
|
+
};
|
|
365
|
+
/**
|
|
366
|
+
* WHAT THE CHANGE PROMISED, COUNTED THE FORECAST'S WAY: the older report's forecast parts that got
|
|
367
|
+
* done — a part whose fix (or one it covers) went away counts whole, one that shrank by its share,
|
|
368
|
+
* work for unread data when the newer report no longer has it — added with the same overlap rules
|
|
369
|
+
* and caps as the forecast itself. The forecast was held to account against fixed pages; this is
|
|
370
|
+
* the same arithmetic for the fix you actually made.
|
|
371
|
+
*/
|
|
372
|
+
/**
|
|
373
|
+
* HOW MUCH OF EACH OLDER PATTERN IS STILL THERE, BY ITS OWN SITES: the share of its cost on sites
|
|
374
|
+
* whose code the newer report still flags with the same kind (matched by the code, not the line
|
|
375
|
+
* number an edit above moves). "Waits in a row" in both reports can be two different sets of calls —
|
|
376
|
+
* the chains fixed, a new one between the batches — and its title alone would say it only shrank.
|
|
377
|
+
*/
|
|
378
|
+
function still_there(a, b) {
|
|
379
|
+
const out = /* @__PURE__ */ new Map();
|
|
380
|
+
const code_of = (s) => (s.code ?? "").trim();
|
|
381
|
+
const cost = (s) => s.wait_ms ?? s.cpu_ms ?? 0;
|
|
382
|
+
for (const p of a ?? []) {
|
|
383
|
+
const left = new Set((b ?? []).filter((q) => q.kind === p.kind).flatMap((q) => q.sites.map(code_of)));
|
|
384
|
+
const coded = p.sites.filter((s) => code_of(s));
|
|
385
|
+
const total = coded.reduce((t, s) => t + cost(s), 0);
|
|
386
|
+
if (!(total > 0)) continue;
|
|
387
|
+
const kept = coded.filter((s) => left.has(code_of(s))).reduce((t, s) => t + cost(s), 0);
|
|
388
|
+
out.set(bare_title(p.title), kept / total);
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
function promised_by(fa, fb, patterns, left, b_clean = false) {
|
|
393
|
+
const find = (title) => patterns.find((p) => bare_title(p.title) === bare_title(title));
|
|
394
|
+
const still_unread = /* @__PURE__ */ new Set();
|
|
395
|
+
const is_unread = (t) => t.startsWith("Delete work for data nothing reads");
|
|
396
|
+
for (const p of [...fb?.parts ?? [], ...fb?.answers?.parts ?? []]) {
|
|
397
|
+
if (p.kind === "unread-work") still_unread.add(p.title);
|
|
398
|
+
for (const w of p.with ?? []) if (is_unread(w)) still_unread.add(w);
|
|
399
|
+
}
|
|
400
|
+
const b_known = !!fb || b_clean;
|
|
401
|
+
const done = [];
|
|
402
|
+
for (const part of fa.parts) {
|
|
403
|
+
if (part.kind === "unread-work") {
|
|
404
|
+
if (b_known && !still_unread.has(part.title)) done.push(part);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
let share = 0;
|
|
408
|
+
for (const t of [part.title, ...part.with ?? []]) {
|
|
409
|
+
const d = find(t);
|
|
410
|
+
if (!d) continue;
|
|
411
|
+
let frac = 0;
|
|
412
|
+
if (d.status === "fixed") frac = 1;
|
|
413
|
+
else if (d.status === "better" && d.a_save > 0) {
|
|
414
|
+
const l = left?.get(bare_title(t));
|
|
415
|
+
frac = Math.max(Math.min(1, Math.max(0, (d.a_save - d.b_save) / d.a_save)), l === void 0 ? 0 : 1 - l);
|
|
416
|
+
}
|
|
417
|
+
if (frac <= 0) continue;
|
|
418
|
+
share = Math.max(share, t === part.title || part.ms <= 0 ? frac : Math.min(1, frac * d.a_save / part.ms));
|
|
419
|
+
}
|
|
420
|
+
if (share > 0) done.push({
|
|
421
|
+
ms: part.ms * share,
|
|
422
|
+
wait: part.wait,
|
|
423
|
+
kind: part.kind
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return fa.now_ms - saving_of(done, fa.caps ?? {}, fa.now_ms).after;
|
|
427
|
+
}
|
|
428
|
+
/** The patterns a change fixed or shrank, their expected saving summed, against the median render's
|
|
429
|
+
* measured change. A saving well under the prediction usually moved somewhere (another line got
|
|
430
|
+
* slower) or drowned in run-to-run noise; well over it, the fix removed more than the pattern saw. */
|
|
431
|
+
function fix_check_of(patterns, a, b, fa, fb, left) {
|
|
432
|
+
if (a.trigger !== "page" || b.trigger !== "page" || !a.runs?.length || !b.runs?.length) return void 0;
|
|
433
|
+
const predicted = fa?.parts.length ? promised_by(fa, fb, patterns, left, !fb && (b.run_status ?? 200) === 200) : summed_promise(patterns);
|
|
434
|
+
if (predicted < 2) return void 0;
|
|
435
|
+
const measured = median(a.runs) - median(b.runs);
|
|
436
|
+
const verdict = measured >= predicted * .6 && measured <= predicted * 1.6 ? "as-expected" : measured < predicted * .6 ? "less" : "more";
|
|
437
|
+
return {
|
|
438
|
+
predicted_ms: round2(predicted),
|
|
439
|
+
measured_ms: round2(measured),
|
|
440
|
+
verdict
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
444
|
+
/** finding codes about the recording's conditions, not the page's code (compare leaves them out) */
|
|
445
|
+
const RUN_STATE_CODES = /* @__PURE__ */ new Set([
|
|
446
|
+
"cold-start",
|
|
447
|
+
"cold-instance",
|
|
448
|
+
"profiler-overhead",
|
|
449
|
+
"heap-filled-before",
|
|
450
|
+
"heap-near-limit",
|
|
451
|
+
"budget",
|
|
452
|
+
"dev-mode",
|
|
453
|
+
"low-confidence",
|
|
454
|
+
"busy-instance",
|
|
455
|
+
"runs-set-aside",
|
|
456
|
+
"summary",
|
|
457
|
+
"phases"
|
|
458
|
+
]);
|
|
459
|
+
/** warnings that fire when one number of the recording passes a cut (report.ts): the number, and the cut */
|
|
460
|
+
const NEAR_CUT = { "loop-stall": {
|
|
461
|
+
value: (m) => m.loop_delay?.p99,
|
|
462
|
+
at: 50
|
|
463
|
+
} };
|
|
464
|
+
const median = (xs) => xs.length ? [...xs].sort((x, y) => x - y)[Math.floor(xs.length / 2)] : 0;
|
|
465
|
+
function label_of(m) {
|
|
466
|
+
if (m.trigger === "page") return `page ${m.page} ×${m.runs?.length ?? 0}`;
|
|
467
|
+
if (m.trigger === "request") return `request ${m.request?.path ?? ""}`;
|
|
468
|
+
if (m.trigger === "trap") return `caught ${m.request?.path ?? ""}`;
|
|
469
|
+
return `${Math.round(m.duration_ms / 1e3)}s window`;
|
|
470
|
+
}
|
|
471
|
+
function delta_rows(a, b, by) {
|
|
472
|
+
const map = /* @__PURE__ */ new Map();
|
|
473
|
+
for (const f of a) map.set(f[by], {
|
|
474
|
+
key: f[by],
|
|
475
|
+
name: f.name,
|
|
476
|
+
file: f.url,
|
|
477
|
+
line: f.line,
|
|
478
|
+
a_self: f.self_ms,
|
|
479
|
+
b_self: 0,
|
|
480
|
+
d_self: -f.self_ms,
|
|
481
|
+
a_total: f.total_ms,
|
|
482
|
+
b_total: 0,
|
|
483
|
+
d_total: -f.total_ms,
|
|
484
|
+
a_calls: f.calls ?? null,
|
|
485
|
+
b_calls: null,
|
|
486
|
+
only: "a"
|
|
487
|
+
});
|
|
488
|
+
for (const f of b) {
|
|
489
|
+
const row = map.get(f[by]);
|
|
490
|
+
if (row) {
|
|
491
|
+
row.file = f.url;
|
|
492
|
+
row.line = f.line;
|
|
493
|
+
row.b_self = f.self_ms;
|
|
494
|
+
row.b_total = f.total_ms;
|
|
495
|
+
row.b_calls = f.calls ?? null;
|
|
496
|
+
row.d_self = round2(f.self_ms - row.a_self);
|
|
497
|
+
row.d_total = round2(f.total_ms - row.a_total);
|
|
498
|
+
delete row.only;
|
|
499
|
+
} else map.set(f[by], {
|
|
500
|
+
key: f[by],
|
|
501
|
+
name: f.name,
|
|
502
|
+
file: f.url,
|
|
503
|
+
line: f.line,
|
|
504
|
+
a_self: 0,
|
|
505
|
+
b_self: f.self_ms,
|
|
506
|
+
d_self: f.self_ms,
|
|
507
|
+
a_total: 0,
|
|
508
|
+
b_total: f.total_ms,
|
|
509
|
+
d_total: f.total_ms,
|
|
510
|
+
a_calls: null,
|
|
511
|
+
b_calls: f.calls ?? null,
|
|
512
|
+
only: "b"
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return [...map.values()].sort((x, y) => Math.abs(y.d_self) - Math.abs(x.d_self) || Math.abs(y.d_total) - Math.abs(x.d_total));
|
|
516
|
+
}
|
|
517
|
+
function compare_reports(a, b) {
|
|
518
|
+
const drill = drill_deltas(a.drill, b.drill);
|
|
519
|
+
const order = order_of(a, b);
|
|
520
|
+
const A = a.analysis;
|
|
521
|
+
const B = b.analysis;
|
|
522
|
+
const ra = renders_of(a.meta);
|
|
523
|
+
const rb = renders_of(b.meta);
|
|
524
|
+
const per = (list, r) => r === 1 ? [...list] : list.map((f) => ({
|
|
525
|
+
...f,
|
|
526
|
+
self_ms: round2(f.self_ms / r),
|
|
527
|
+
total_ms: round2(f.total_ms / r)
|
|
528
|
+
}));
|
|
529
|
+
const gc_makers = /* @__PURE__ */ new Map();
|
|
530
|
+
const mb = (n) => round2(n / 1048576);
|
|
531
|
+
for (const m of a.gc?.makers ?? []) {
|
|
532
|
+
const al = mb(m.allocated / ra);
|
|
533
|
+
const g = round2(m.gc_ms / ra);
|
|
534
|
+
gc_makers.set(`${m.name}|${m.caller ?? ""}`, {
|
|
535
|
+
name: m.name,
|
|
536
|
+
caller: m.caller ?? null,
|
|
537
|
+
component: m.component,
|
|
538
|
+
a_mb: al,
|
|
539
|
+
b_mb: 0,
|
|
540
|
+
d_mb: -al,
|
|
541
|
+
a_gc_ms: g,
|
|
542
|
+
b_gc_ms: 0,
|
|
543
|
+
d_gc_ms: -g,
|
|
544
|
+
only: "a"
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
for (const m of b.gc?.makers ?? []) {
|
|
548
|
+
const k = `${m.name}|${m.caller ?? ""}`;
|
|
549
|
+
const r = gc_makers.get(k);
|
|
550
|
+
const al = mb(m.allocated / rb);
|
|
551
|
+
const g = round2(m.gc_ms / rb);
|
|
552
|
+
if (r) {
|
|
553
|
+
r.b_mb = al;
|
|
554
|
+
r.d_mb = round2(r.b_mb - r.a_mb);
|
|
555
|
+
r.b_gc_ms = g;
|
|
556
|
+
r.d_gc_ms = round2(g - r.a_gc_ms);
|
|
557
|
+
delete r.only;
|
|
558
|
+
} else gc_makers.set(k, {
|
|
559
|
+
name: m.name,
|
|
560
|
+
caller: m.caller ?? null,
|
|
561
|
+
component: m.component,
|
|
562
|
+
a_mb: 0,
|
|
563
|
+
b_mb: al,
|
|
564
|
+
d_mb: al,
|
|
565
|
+
a_gc_ms: 0,
|
|
566
|
+
b_gc_ms: g,
|
|
567
|
+
d_gc_ms: g,
|
|
568
|
+
only: "b"
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
const row = (label, x, y, unit) => ({
|
|
572
|
+
label,
|
|
573
|
+
a: round2(x),
|
|
574
|
+
b: round2(y),
|
|
575
|
+
d: round2(y - x),
|
|
576
|
+
unit
|
|
577
|
+
});
|
|
578
|
+
const summary = [];
|
|
579
|
+
if (a.meta.trigger === "page" && b.meta.trigger === "page") summary.push(row("render (median run)", median(a.meta.runs ?? []), median(b.meta.runs ?? []), "ms"));
|
|
580
|
+
else if (a.meta.trigger === "request" && b.meta.trigger === "request") summary.push(row("request", a.meta.request?.ms ?? 0, b.meta.request?.ms ?? 0, "ms"));
|
|
581
|
+
summary.push(row("CPU busy (per render)", A.busy_ms / ra, B.busy_ms / rb, "ms"));
|
|
582
|
+
summary.push(row("garbage collection (per render)", (a.gc?.summary.total_ms ?? A.gc_ms) / ra, (b.gc?.summary.total_ms ?? B.gc_ms) / rb, "ms"));
|
|
583
|
+
if (a.gc || b.gc) summary.push(row("allocated (per render)", (a.gc?.summary.allocated_mb ?? 0) / ra, (b.gc?.summary.allocated_mb ?? 0) / rb, "MB"));
|
|
584
|
+
if (A.timeline && B.timeline) summary.push(row("waiting (I/O)", A.timeline.wait_ms, B.timeline.wait_ms, "ms"));
|
|
585
|
+
const net_ms = (m) => {
|
|
586
|
+
const own = m.requests.filter((r) => r.internal || m.trigger !== "page");
|
|
587
|
+
return own.length ? Math.max(...own.map((r) => r.net_ms)) : 0;
|
|
588
|
+
};
|
|
589
|
+
const net_n = (m) => {
|
|
590
|
+
const own = m.requests.filter((r) => r.internal || m.trigger !== "page");
|
|
591
|
+
return own.length ? Math.max(...own.map((r) => r.net_count)) : 0;
|
|
592
|
+
};
|
|
593
|
+
summary.push(row("outbound calls", net_n(a.meta), net_n(b.meta), "n"));
|
|
594
|
+
summary.push(row("outbound time", net_ms(a.meta), net_ms(b.meta), "ms"));
|
|
595
|
+
const og = (m) => own_requests(m).find((r) => r.og)?.og;
|
|
596
|
+
const oa = og(a.meta);
|
|
597
|
+
const ob = og(b.meta);
|
|
598
|
+
if (oa || ob) {
|
|
599
|
+
summary.push(row("ogygia transform", oa?.transform_ms ?? 0, ob?.transform_ms ?? 0, "ms"));
|
|
600
|
+
summary.push(row("page seed", (oa?.seed_bytes ?? 0) / 1024, (ob?.seed_bytes ?? 0) / 1024, "KB"));
|
|
601
|
+
summary.push(row("island props", (oa?.tail_bytes ?? 0) / 1024, (ob?.tail_bytes ?? 0) / 1024, "KB"));
|
|
602
|
+
}
|
|
603
|
+
summary.push(row("components seen", A.components.length, B.components.length, "n"));
|
|
604
|
+
const phase_map = /* @__PURE__ */ new Map();
|
|
605
|
+
for (const p of A.timeline?.phases ?? []) phase_map.set(p.phase, {
|
|
606
|
+
a: p.cpu_ms + p.wait_ms,
|
|
607
|
+
b: 0
|
|
608
|
+
});
|
|
609
|
+
for (const p of B.timeline?.phases ?? []) {
|
|
610
|
+
const r = phase_map.get(p.phase) ?? {
|
|
611
|
+
a: 0,
|
|
612
|
+
b: 0
|
|
613
|
+
};
|
|
614
|
+
r.b = p.cpu_ms + p.wait_ms;
|
|
615
|
+
phase_map.set(p.phase, r);
|
|
616
|
+
}
|
|
617
|
+
const phases = [...phase_map.entries()].map(([phase, r]) => ({
|
|
618
|
+
phase,
|
|
619
|
+
label: PHASE_LABEL[phase],
|
|
620
|
+
a: round2(r.a),
|
|
621
|
+
b: round2(r.b),
|
|
622
|
+
d: round2(r.b - r.a)
|
|
623
|
+
})).sort((x, y) => Math.abs(y.d) - Math.abs(x.d));
|
|
624
|
+
const code_of = (f) => {
|
|
625
|
+
const colon = f.indexOf(":");
|
|
626
|
+
return colon === -1 ? f : f.slice(0, colon);
|
|
627
|
+
};
|
|
628
|
+
const a_codes = new Set(a.findings.map(code_of));
|
|
629
|
+
const b_codes = new Set(b.findings.map(code_of));
|
|
630
|
+
const near_cut = (code) => {
|
|
631
|
+
const cut = NEAR_CUT[code];
|
|
632
|
+
if (!cut) return false;
|
|
633
|
+
const x = cut.value(a.meta);
|
|
634
|
+
const y = cut.value(b.meta);
|
|
635
|
+
return x !== void 0 && y !== void 0 && Math.abs(x - cut.at) <= cut.at * .3 && Math.abs(y - cut.at) <= cut.at * .3;
|
|
636
|
+
};
|
|
637
|
+
const of_the_code = (f) => !RUN_STATE_CODES.has(code_of(f)) && !near_cut(code_of(f));
|
|
638
|
+
const paths = /* @__PURE__ */ new Map();
|
|
639
|
+
for (const g of A.paths ?? []) {
|
|
640
|
+
const ms = round2(g.ms / ra);
|
|
641
|
+
paths.set(g.owner.name, {
|
|
642
|
+
owner: g.owner.name,
|
|
643
|
+
file: g.owner.url,
|
|
644
|
+
line: g.owner.line,
|
|
645
|
+
a_ms: ms,
|
|
646
|
+
b_ms: 0,
|
|
647
|
+
d_ms: -ms,
|
|
648
|
+
a_fns: g.fns.map((f) => f.name),
|
|
649
|
+
b_fns: [],
|
|
650
|
+
only: "a"
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
for (const g0 of B.paths ?? []) {
|
|
654
|
+
const g = {
|
|
655
|
+
...g0,
|
|
656
|
+
ms: round2(g0.ms / rb)
|
|
657
|
+
};
|
|
658
|
+
const row = paths.get(g.owner.name);
|
|
659
|
+
if (row) {
|
|
660
|
+
row.b_ms = g.ms;
|
|
661
|
+
row.d_ms = round2(g.ms - row.a_ms);
|
|
662
|
+
row.b_fns = g.fns.map((f) => f.name);
|
|
663
|
+
row.file = g.owner.url;
|
|
664
|
+
row.line = g.owner.line;
|
|
665
|
+
delete row.only;
|
|
666
|
+
} else paths.set(g.owner.name, {
|
|
667
|
+
owner: g.owner.name,
|
|
668
|
+
file: g.owner.url,
|
|
669
|
+
line: g.owner.line,
|
|
670
|
+
a_ms: 0,
|
|
671
|
+
b_ms: g.ms,
|
|
672
|
+
d_ms: g.ms,
|
|
673
|
+
a_fns: [],
|
|
674
|
+
b_fns: g.fns.map((f) => f.name),
|
|
675
|
+
only: "b"
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
const patterns = pattern_deltas(a.patterns, b.patterns, ra, rb);
|
|
679
|
+
const checked = fix_check_of(patterns, a.meta, b.meta, a.forecast, b.forecast, still_there(a.patterns, b.patterns));
|
|
680
|
+
const fix_check = checked && order?.earlier === "a" ? {
|
|
681
|
+
...checked,
|
|
682
|
+
skewed: true
|
|
683
|
+
} : checked;
|
|
684
|
+
return mark_shifted({
|
|
685
|
+
a: {
|
|
686
|
+
id: a.meta.id,
|
|
687
|
+
label: label_of(a.meta),
|
|
688
|
+
created: a.meta.created
|
|
689
|
+
},
|
|
690
|
+
b: {
|
|
691
|
+
id: b.meta.id,
|
|
692
|
+
label: label_of(b.meta),
|
|
693
|
+
created: b.meta.created
|
|
694
|
+
},
|
|
695
|
+
summary,
|
|
696
|
+
phases,
|
|
697
|
+
components: delta_rows(per(A.components, ra), per(B.components, rb), "name"),
|
|
698
|
+
functions: delta_rows(per(A.functions.slice(0, 120), ra), per(B.functions.slice(0, 120), rb), "key").slice(0, 60),
|
|
699
|
+
findings: {
|
|
700
|
+
added: [...new Set(b.findings.filter((f) => of_the_code(f) && !a_codes.has(code_of(f))))],
|
|
701
|
+
gone: [...new Set(a.findings.filter((f) => of_the_code(f) && !b_codes.has(code_of(f))))]
|
|
702
|
+
},
|
|
703
|
+
paths: [...paths.values()].sort((x, y) => Math.abs(y.d_ms) - Math.abs(x.d_ms)),
|
|
704
|
+
gc_makers: [...gc_makers.values()].filter((m) => Math.abs(m.d_mb) >= .5 || Math.abs(m.d_gc_ms) >= .5).sort((x, y) => Math.abs(y.d_gc_ms) - Math.abs(x.d_gc_ms) || Math.abs(y.d_mb) - Math.abs(x.d_mb)),
|
|
705
|
+
patterns,
|
|
706
|
+
...fix_check ? { fix_check } : {},
|
|
707
|
+
lines: line_deltas(a.ledger, b.ledger, renders_of(a.meta), renders_of(b.meta)),
|
|
708
|
+
...drill.length ? { drill } : {},
|
|
709
|
+
...order ? { order } : {},
|
|
710
|
+
...render_same_of(a.meta, b.meta)
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
/** Page-mode reports grouped by page, oldest first, with each run's median — the history line. */
|
|
714
|
+
function page_history(metas, score_of) {
|
|
715
|
+
const by_page = /* @__PURE__ */ new Map();
|
|
716
|
+
for (const m of metas) {
|
|
717
|
+
if (m.trigger !== "page" || !m.page || !m.runs?.length) continue;
|
|
718
|
+
const list = by_page.get(m.page) ?? [];
|
|
719
|
+
const score = score_of?.(m.id);
|
|
720
|
+
list.push({
|
|
721
|
+
id: m.id,
|
|
722
|
+
created: m.created,
|
|
723
|
+
median: round2(median(m.runs)),
|
|
724
|
+
...score !== void 0 ? { score } : {}
|
|
725
|
+
});
|
|
726
|
+
by_page.set(m.page, list);
|
|
727
|
+
}
|
|
728
|
+
return [...by_page.entries()].map(([page, points]) => ({
|
|
729
|
+
page,
|
|
730
|
+
points: points.sort((x, y) => x.created - y.created)
|
|
731
|
+
})).filter((h) => h.points.length >= 1).sort((x, y) => y.points.at(-1).created - x.points.at(-1).created);
|
|
732
|
+
}
|
|
733
|
+
export { compare_reports, drill_deltas, fix_check_of, island_files_diff, line_deltas, mark_shifted, order_of, page_history, pattern_deltas, still_there, vitals_moved };
|