@ogygia/devtools 0.0.0 → 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 +24 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.js +24 -0
- package/dist/devtools/BoundaryOverlay.svelte +396 -0
- package/dist/devtools/BoundaryOverlay.svelte.d.ts +19 -0
- package/dist/devtools/Devtools.svelte +663 -0
- package/dist/devtools/Devtools.svelte.d.ts +15 -0
- package/dist/devtools/HubTab.svelte +206 -0
- package/dist/devtools/HubTab.svelte.d.ts +11 -0
- package/dist/devtools/HydrationTab.svelte +237 -0
- package/dist/devtools/HydrationTab.svelte.d.ts +13 -0
- package/dist/devtools/IslandDetail.svelte +615 -0
- package/dist/devtools/IslandDetail.svelte.d.ts +15 -0
- package/dist/devtools/LedgerTab.svelte +287 -0
- package/dist/devtools/LedgerTab.svelte.d.ts +11 -0
- package/dist/devtools/LensTab.svelte +290 -0
- package/dist/devtools/LensTab.svelte.d.ts +19 -0
- package/dist/devtools/NavTab.svelte +229 -0
- package/dist/devtools/NavTab.svelte.d.ts +11 -0
- package/dist/devtools/PageTab.svelte +1122 -0
- package/dist/devtools/PageTab.svelte.d.ts +15 -0
- package/dist/devtools/ProfilerTab.svelte +755 -0
- package/dist/devtools/ProfilerTab.svelte.d.ts +15 -0
- package/dist/devtools/PropsTree.svelte +155 -0
- package/dist/devtools/PropsTree.svelte.d.ts +15 -0
- package/dist/devtools/SessionTab.svelte +796 -0
- package/dist/devtools/SessionTab.svelte.d.ts +11 -0
- package/dist/devtools/TimelineTab.svelte +267 -0
- package/dist/devtools/TimelineTab.svelte.d.ts +11 -0
- package/dist/devtools/WireTab.svelte +149 -0
- package/dist/devtools/WireTab.svelte.d.ts +11 -0
- package/dist/devtools/bytes.d.ts +70 -0
- package/dist/devtools/bytes.js +82 -0
- package/dist/devtools/cache-headers.d.ts +20 -0
- package/dist/devtools/cache-headers.js +81 -0
- package/dist/devtools/highlight.d.ts +7 -0
- package/dist/devtools/highlight.js +83 -0
- package/dist/devtools/hydration.d.ts +48 -0
- package/dist/devtools/hydration.js +93 -0
- package/dist/devtools/index.d.ts +3 -0
- package/dist/devtools/index.js +3 -0
- package/dist/devtools/ledger-dom.d.ts +7 -0
- package/dist/devtools/ledger-dom.js +151 -0
- package/dist/devtools/leftovers.d.ts +77 -0
- package/dist/devtools/leftovers.js +319 -0
- package/dist/devtools/names.d.ts +1 -0
- package/dist/devtools/names.js +34 -0
- package/dist/devtools/page.d.ts +96 -0
- package/dist/devtools/page.js +957 -0
- package/dist/devtools/profile-anchors.d.ts +22 -0
- package/dist/devtools/profile-anchors.js +135 -0
- package/dist/devtools/profile-store.d.ts +175 -0
- package/dist/devtools/profile-store.js +239 -0
- package/dist/devtools/regions.d.ts +77 -0
- package/dist/devtools/regions.js +132 -0
- package/dist/devtools/session.d.ts +31 -0
- package/dist/devtools/session.js +455 -0
- package/dist/devtools/since-load.d.ts +50 -0
- package/dist/devtools/since-load.js +89 -0
- package/dist/devtools/source-lines.d.ts +24 -0
- package/dist/devtools/source-lines.js +106 -0
- package/dist/devtools/styles.d.ts +52 -0
- package/dist/devtools/styles.js +248 -0
- package/dist/devtools/testing-engine.d.ts +83 -0
- package/dist/devtools/testing-engine.js +220 -0
- package/dist/devtools/ui-app.d.ts +5 -0
- package/dist/devtools/ui-app.js +69 -0
- package/dist/devtools/ui.d.ts +50 -0
- package/dist/devtools/ui.js +175 -0
- package/dist/profiler/alloc.d.ts +75 -0
- package/dist/profiler/alloc.js +76 -0
- package/dist/profiler/analyze.d.ts +437 -0
- package/dist/profiler/analyze.js +1941 -0
- package/dist/profiler/app-path.d.ts +7 -0
- package/dist/profiler/app-path.js +26 -0
- package/dist/profiler/async-io.d.ts +58 -0
- package/dist/profiler/async-io.js +187 -0
- package/dist/profiler/beacon-standalone.d.ts +4 -0
- package/dist/profiler/beacon-standalone.js +409 -0
- package/dist/profiler/browser-findings.d.ts +25 -0
- package/dist/profiler/browser-findings.js +260 -0
- package/dist/profiler/byte-strip.d.ts +58 -0
- package/dist/profiler/byte-strip.js +228 -0
- package/dist/profiler/client-files.d.ts +16 -0
- package/dist/profiler/client-files.js +60 -0
- package/dist/profiler/client-windows.d.ts +49 -0
- package/dist/profiler/client-windows.js +95 -0
- package/dist/profiler/client.d.ts +40 -0
- package/dist/profiler/client.js +82 -0
- package/dist/profiler/compare.d.ts +308 -0
- package/dist/profiler/compare.js +733 -0
- package/dist/profiler/compress.d.ts +10 -0
- package/dist/profiler/compress.js +36 -0
- package/dist/profiler/contention.d.ts +60 -0
- package/dist/profiler/contention.js +86 -0
- package/dist/profiler/crypto.d.ts +19 -0
- package/dist/profiler/crypto.js +101 -0
- package/dist/profiler/doc-diff.d.ts +47 -0
- package/dist/profiler/doc-diff.js +214 -0
- package/dist/profiler/dom-diff.d.ts +43 -0
- package/dist/profiler/dom-diff.js +121 -0
- package/dist/profiler/drill.d.ts +89 -0
- package/dist/profiler/drill.js +553 -0
- package/dist/profiler/embedded-maps.d.ts +37 -0
- package/dist/profiler/embedded-maps.js +135 -0
- package/dist/profiler/env.d.ts +6 -0
- package/dist/profiler/env.js +10 -0
- package/dist/profiler/forecast.d.ts +76 -0
- package/dist/profiler/forecast.js +225 -0
- package/dist/profiler/frames.d.ts +11 -0
- package/dist/profiler/frames.js +42 -0
- package/dist/profiler/gc.d.ts +144 -0
- package/dist/profiler/gc.js +271 -0
- package/dist/profiler/hole-slots.d.ts +31 -0
- package/dist/profiler/hole-slots.js +90 -0
- package/dist/profiler/index.d.ts +288 -0
- package/dist/profiler/index.js +4921 -0
- package/dist/profiler/insights.d.ts +96 -0
- package/dist/profiler/insights.js +239 -0
- package/dist/profiler/ledger.d.ts +121 -0
- package/dist/profiler/ledger.js +440 -0
- package/dist/profiler/lineage.d.ts +81 -0
- package/dist/profiler/lineage.js +359 -0
- package/dist/profiler/module-map.d.ts +12 -0
- package/dist/profiler/module-map.js +94 -0
- package/dist/profiler/net.d.ts +169 -0
- package/dist/profiler/net.js +586 -0
- package/dist/profiler/on-demand.d.ts +11 -0
- package/dist/profiler/on-demand.js +15 -0
- package/dist/profiler/own-requests.d.ts +9 -0
- package/dist/profiler/own-requests.js +17 -0
- package/dist/profiler/patterns.d.ts +476 -0
- package/dist/profiler/patterns.js +2203 -0
- package/dist/profiler/profiler-router.d.ts +245 -0
- package/dist/profiler/profiler-router.js +113 -0
- package/dist/profiler/refresh-stub.d.ts +2 -0
- package/dist/profiler/refresh-stub.js +37 -0
- package/dist/profiler/report.d.ts +1404 -0
- package/dist/profiler/report.js +2638 -0
- package/dist/profiler/river.d.ts +65 -0
- package/dist/profiler/river.js +298 -0
- package/dist/profiler/route-files.d.ts +22 -0
- package/dist/profiler/route-files.js +55 -0
- package/dist/profiler/route-imports.d.ts +14 -0
- package/dist/profiler/route-imports.js +83 -0
- package/dist/profiler/saving.d.ts +22 -0
- package/dist/profiler/saving.js +42 -0
- package/dist/profiler/score.d.ts +109 -0
- package/dist/profiler/score.js +261 -0
- package/dist/profiler/session-cookie.d.ts +28 -0
- package/dist/profiler/session-cookie.js +45 -0
- package/dist/profiler/sink.d.ts +81 -0
- package/dist/profiler/sink.js +124 -0
- package/dist/profiler/site-fixes.d.ts +25 -0
- package/dist/profiler/site-fixes.js +62 -0
- package/dist/profiler/source-scan.d.ts +210 -0
- package/dist/profiler/source-scan.js +1724 -0
- package/dist/profiler/span.d.ts +90 -0
- package/dist/profiler/span.js +109 -0
- package/dist/profiler/stacks.d.ts +43 -0
- package/dist/profiler/stacks.js +107 -0
- package/dist/profiler/standalone.d.ts +27 -0
- package/dist/profiler/standalone.js +98 -0
- package/dist/profiler/steps.d.ts +39 -0
- package/dist/profiler/steps.js +74 -0
- package/dist/profiler/storage/index.d.ts +87 -0
- package/dist/profiler/storage/index.js +76 -0
- package/dist/profiler/storage/postgres.d.ts +12 -0
- package/dist/profiler/storage/postgres.js +94 -0
- package/dist/profiler/storage/redis.d.ts +19 -0
- package/dist/profiler/storage/redis.js +148 -0
- package/dist/profiler/storage/sqlite.d.ts +2 -0
- package/dist/profiler/storage/sqlite.js +85 -0
- package/dist/profiler/storage/visits.d.ts +4 -0
- package/dist/profiler/storage/visits.js +6 -0
- package/dist/profiler/stream-tail.d.ts +26 -0
- package/dist/profiler/stream-tail.js +74 -0
- package/dist/profiler/timeline.d.ts +279 -0
- package/dist/profiler/timeline.js +690 -0
- package/dist/profiler/ui/AllocStrip.svelte +146 -0
- package/dist/profiler/ui/AllocStrip.svelte.d.ts +17 -0
- package/dist/profiler/ui/Awaits.svelte +363 -0
- package/dist/profiler/ui/Awaits.svelte.d.ts +16 -0
- package/dist/profiler/ui/ByteStrip.svelte +182 -0
- package/dist/profiler/ui/ByteStrip.svelte.d.ts +18 -0
- package/dist/profiler/ui/Compare.svelte +24 -0
- package/dist/profiler/ui/Compare.svelte.d.ts +16 -0
- package/dist/profiler/ui/CompareBody.svelte +429 -0
- package/dist/profiler/ui/CompareBody.svelte.d.ts +8 -0
- package/dist/profiler/ui/ComponentsTable.svelte +265 -0
- package/dist/profiler/ui/ComponentsTable.svelte.d.ts +20 -0
- package/dist/profiler/ui/Dashboard.svelte +492 -0
- package/dist/profiler/ui/Dashboard.svelte.d.ts +31 -0
- package/dist/profiler/ui/DataRiver.svelte +206 -0
- package/dist/profiler/ui/DataRiver.svelte.d.ts +13 -0
- package/dist/profiler/ui/DomTravel.svelte +207 -0
- package/dist/profiler/ui/DomTravel.svelte.d.ts +9 -0
- package/dist/profiler/ui/DownloadHtml.svelte +52 -0
- package/dist/profiler/ui/DownloadHtml.svelte.d.ts +10 -0
- package/dist/profiler/ui/Drill.svelte +276 -0
- package/dist/profiler/ui/Drill.svelte.d.ts +12 -0
- package/dist/profiler/ui/ExportButton.svelte +59 -0
- package/dist/profiler/ui/ExportButton.svelte.d.ts +8 -0
- package/dist/profiler/ui/Flame.svelte +222 -0
- package/dist/profiler/ui/Flame.svelte.d.ts +12 -0
- package/dist/profiler/ui/FrameDetails.svelte +308 -0
- package/dist/profiler/ui/FrameDetails.svelte.d.ts +16 -0
- package/dist/profiler/ui/FunctionsTable.svelte +156 -0
- package/dist/profiler/ui/FunctionsTable.svelte.d.ts +17 -0
- package/dist/profiler/ui/IslandHeatmap.svelte +151 -0
- package/dist/profiler/ui/IslandHeatmap.svelte.d.ts +16 -0
- package/dist/profiler/ui/IslandsTable.svelte +448 -0
- package/dist/profiler/ui/IslandsTable.svelte.d.ts +16 -0
- package/dist/profiler/ui/KeepLocal.svelte +46 -0
- package/dist/profiler/ui/KeepLocal.svelte.d.ts +8 -0
- package/dist/profiler/ui/Ledger.svelte +196 -0
- package/dist/profiler/ui/Ledger.svelte.d.ts +18 -0
- package/dist/profiler/ui/LocalCompare.svelte +48 -0
- package/dist/profiler/ui/LocalCompare.svelte.d.ts +8 -0
- package/dist/profiler/ui/LocalReports.svelte +68 -0
- package/dist/profiler/ui/LocalReports.svelte.d.ts +7 -0
- package/dist/profiler/ui/Login.svelte +13 -0
- package/dist/profiler/ui/Login.svelte.d.ts +12 -0
- package/dist/profiler/ui/LoginForm.svelte +42 -0
- package/dist/profiler/ui/LoginForm.svelte.d.ts +7 -0
- package/dist/profiler/ui/Message.svelte +10 -0
- package/dist/profiler/ui/Message.svelte.d.ts +12 -0
- package/dist/profiler/ui/OnThisPage.svelte +137 -0
- package/dist/profiler/ui/OnThisPage.svelte.d.ts +3 -0
- package/dist/profiler/ui/OneClock.svelte +297 -0
- package/dist/profiler/ui/OneClock.svelte.d.ts +27 -0
- package/dist/profiler/ui/Panel.svelte +10 -0
- package/dist/profiler/ui/Panel.svelte.d.ts +16 -0
- package/dist/profiler/ui/Paths.svelte +269 -0
- package/dist/profiler/ui/Paths.svelte.d.ts +15 -0
- package/dist/profiler/ui/Patterns.svelte +364 -0
- package/dist/profiler/ui/Patterns.svelte.d.ts +15 -0
- package/dist/profiler/ui/PermalinkGate.svelte +143 -0
- package/dist/profiler/ui/PermalinkGate.svelte.d.ts +10 -0
- package/dist/profiler/ui/RenderSteps.svelte +218 -0
- package/dist/profiler/ui/RenderSteps.svelte.d.ts +13 -0
- package/dist/profiler/ui/Report.svelte +28 -0
- package/dist/profiler/ui/Report.svelte.d.ts +20 -0
- package/dist/profiler/ui/ReportBody.svelte +2592 -0
- package/dist/profiler/ui/ReportBody.svelte.d.ts +21 -0
- package/dist/profiler/ui/Run.svelte +17 -0
- package/dist/profiler/ui/Run.svelte.d.ts +14 -0
- package/dist/profiler/ui/RunView.svelte +146 -0
- package/dist/profiler/ui/RunView.svelte.d.ts +10 -0
- package/dist/profiler/ui/ScoreCard.svelte +197 -0
- package/dist/profiler/ui/ScoreCard.svelte.d.ts +7 -0
- package/dist/profiler/ui/Scrub.svelte +293 -0
- package/dist/profiler/ui/Scrub.svelte.d.ts +18 -0
- package/dist/profiler/ui/SeedExplainer.svelte +193 -0
- package/dist/profiler/ui/SeedExplainer.svelte.d.ts +16 -0
- package/dist/profiler/ui/ShareLink.svelte +148 -0
- package/dist/profiler/ui/ShareLink.svelte.d.ts +8 -0
- package/dist/profiler/ui/Shell.svelte +50 -0
- package/dist/profiler/ui/Shell.svelte.d.ts +27 -0
- package/dist/profiler/ui/Sidebar.svelte +312 -0
- package/dist/profiler/ui/Sidebar.svelte.d.ts +7 -0
- package/dist/profiler/ui/Site.svelte +20 -0
- package/dist/profiler/ui/Site.svelte.d.ts +23 -0
- package/dist/profiler/ui/SiteView.svelte +385 -0
- package/dist/profiler/ui/SiteView.svelte.d.ts +27 -0
- package/dist/profiler/ui/ThemeSwitch.svelte +78 -0
- package/dist/profiler/ui/ThemeSwitch.svelte.d.ts +3 -0
- package/dist/profiler/ui/Timeline.svelte +672 -0
- package/dist/profiler/ui/Timeline.svelte.d.ts +22 -0
- package/dist/profiler/ui/Treemap.svelte +278 -0
- package/dist/profiler/ui/Treemap.svelte.d.ts +20 -0
- package/dist/profiler/ui/Upload.svelte +19 -0
- package/dist/profiler/ui/Upload.svelte.d.ts +10 -0
- package/dist/profiler/ui/UploadForm.svelte +59 -0
- package/dist/profiler/ui/UploadForm.svelte.d.ts +6 -0
- package/dist/profiler/ui/WaitingTable.svelte +55 -0
- package/dist/profiler/ui/WaitingTable.svelte.d.ts +14 -0
- package/dist/profiler/ui/Waterfall.svelte +371 -0
- package/dist/profiler/ui/Waterfall.svelte.d.ts +7 -0
- package/dist/profiler/ui/WhatIf.svelte +87 -0
- package/dist/profiler/ui/WhatIf.svelte.d.ts +18 -0
- package/dist/profiler/ui/fake-progress.svelte.d.ts +15 -0
- package/dist/profiler/ui/fake-progress.svelte.js +38 -0
- package/dist/profiler/ui/format.d.ts +20 -0
- package/dist/profiler/ui/format.js +71 -0
- package/dist/profiler/ui/permalink.d.ts +9 -0
- package/dist/profiler/ui/permalink.js +108 -0
- package/dist/profiler/ui/press.d.ts +5 -0
- package/dist/profiler/ui/press.js +19 -0
- package/dist/profiler/ui/profiler.css +650 -0
- package/dist/profiler/ui/report-data.d.ts +189 -0
- package/dist/profiler/ui/report-data.js +393 -0
- package/dist/profiler/ui/report-request.d.ts +6 -0
- package/dist/profiler/ui/report-request.js +50 -0
- package/dist/profiler/ui/row-anchor.svelte.d.ts +12 -0
- package/dist/profiler/ui/row-anchor.svelte.js +44 -0
- package/dist/profiler/ui/sort.svelte.d.ts +14 -0
- package/dist/profiler/ui/sort.svelte.js +37 -0
- package/dist/profiler/ui/store.d.ts +45 -0
- package/dist/profiler/ui/store.js +92 -0
- package/dist/profiler/visit.d.ts +389 -0
- package/dist/profiler/visit.js +1002 -0
- package/dist/report.d.ts +5 -0
- package/dist/report.js +4 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +38 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.js +78 -0
- package/package.json +105 -6
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
// src/profiler/timeline.ts
|
|
2
|
+
const PHASE_LABEL = {
|
|
3
|
+
hooks: "hooks",
|
|
4
|
+
load: "load functions",
|
|
5
|
+
remote: "remote functions",
|
|
6
|
+
render: "component render",
|
|
7
|
+
ogygia: "ogygia transform",
|
|
8
|
+
kit: "Kit routing + response",
|
|
9
|
+
other: "other"
|
|
10
|
+
};
|
|
11
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
12
|
+
const KIT_LOAD_RE = /\/runtime\/server\/page\/load_data\.js/;
|
|
13
|
+
/** Kit remote functions (`query` / `command` / `form` / `prerender` from a `.remote.ts`): the app's
|
|
14
|
+
* remote file itself, or Kit's remote runtime that runs it. A remote awaited during SSR — an
|
|
15
|
+
* island's top-level `await getX()`, a `load` calling one — is on the critical path like a load. */
|
|
16
|
+
const KIT_REMOTE_RE = /\/runtime\/(?:app\/server\/remote|server\/remote)\//;
|
|
17
|
+
const APP_REMOTE_FILE_RE = /\.remote\.[jt]s(?:\.js)?$/;
|
|
18
|
+
const KIT_RENDER_RE = /\/runtime\/server\/page\/render\.js/;
|
|
19
|
+
const KIT_RUNTIME_RE = /@sveltejs\/kit\/src\/runtime\/server\//;
|
|
20
|
+
const APP_LOAD_FILE_RE = /(?:^|[/\\])(?:\+(?:page|layout)(?:\.server)?\.[jt]s|_(?:page|layout)(?:\.server)?\.[jt]s\.js)$/;
|
|
21
|
+
const APP_HOOKS_FILE_RE = /(?:^|[/\\])hooks\.server\.[jt]s(?:\.js)?$/;
|
|
22
|
+
/** A load file inside a resolved caller (`load (routes/x/+page.server.ts:44)`) or a frame url. */
|
|
23
|
+
const LOAD_FILE_IN_TEXT_RE = /((?:[^\s()]*[/\\])?[+_](page|layout|error)(\.server)?\.[jt]s(?:\.js)?)(?::\d+)?/;
|
|
24
|
+
const SRC_REL_RE = /[/\\]src[/\\](.*)$/;
|
|
25
|
+
/** The lane a load frame / caller belongs to: `{ file, level, kind }`, or null. */
|
|
26
|
+
function load_lane_of(text) {
|
|
27
|
+
const m = LOAD_FILE_IN_TEXT_RE.exec(text);
|
|
28
|
+
if (!m) return null;
|
|
29
|
+
let file = m[1].replace(/\\/g, "/");
|
|
30
|
+
const rel = SRC_REL_RE.exec(file);
|
|
31
|
+
file = rel ? rel[1] : file.split("/").filter(Boolean).slice(-3).join("/");
|
|
32
|
+
file = file.replace(/(^|\/)_(page|layout|error)((?:\.server)?\.[jt]s)\.js$/, (_, pre, l, ext) => `${pre}+${l}${ext}`);
|
|
33
|
+
return {
|
|
34
|
+
file,
|
|
35
|
+
level: m[2],
|
|
36
|
+
kind: m[3] ? "server" : "universal"
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const OGYGIA_SERVER_RE = /\/(?:ogygia|@ogygia\/core|packages\/core)\/(?:src|dist)\/(?!profiler\/)/;
|
|
40
|
+
/** The Kit phase of ONE frame, or null when the frame says nothing about it. */
|
|
41
|
+
function phase_of_frame(f, hooks) {
|
|
42
|
+
if (f.name === "inject_client_seeds") return "ogygia";
|
|
43
|
+
const url = f.url;
|
|
44
|
+
if (!url) return null;
|
|
45
|
+
const stem = hooks?.get(f.name);
|
|
46
|
+
if (stem && is_in_file(url, stem)) return "hooks";
|
|
47
|
+
if (OGYGIA_SERVER_RE.test(url)) return "ogygia";
|
|
48
|
+
if (APP_REMOTE_FILE_RE.test(url) || KIT_REMOTE_RE.test(url)) return "remote";
|
|
49
|
+
if (f.category === "component" || f.category === "svelte" || KIT_RENDER_RE.test(url)) return "render";
|
|
50
|
+
if (KIT_LOAD_RE.test(url) || APP_LOAD_FILE_RE.test(url)) return "load";
|
|
51
|
+
if (APP_HOOKS_FILE_RE.test(url)) return "hooks";
|
|
52
|
+
if (KIT_RUNTIME_RE.test(url)) return "kit";
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** Walk a stack leaf → root; the first frame that names a phase wins (the deepest decision). */
|
|
56
|
+
function phase_of_stack(frames, hooks) {
|
|
57
|
+
for (const f of frames) {
|
|
58
|
+
const p = phase_of_frame(f, hooks);
|
|
59
|
+
if (p) return p;
|
|
60
|
+
}
|
|
61
|
+
if (hooks?.size) for (const f of frames) {
|
|
62
|
+
if (!f.url) continue;
|
|
63
|
+
for (const stem of hooks.values()) if (is_in_file(f.url, stem)) return "hooks";
|
|
64
|
+
}
|
|
65
|
+
return "other";
|
|
66
|
+
}
|
|
67
|
+
/** does `url` name a file whose stem is `stem` (`…/ds-ssr.ts`, `…/ds-ssr.js`, `ds-ssr.ts`)? */
|
|
68
|
+
function is_in_file(url, stem) {
|
|
69
|
+
const at = url.lastIndexOf(stem + ".");
|
|
70
|
+
if (at === -1) return false;
|
|
71
|
+
const before = url.charCodeAt(at - 1);
|
|
72
|
+
return at === 0 || before === 47 || before === 92;
|
|
73
|
+
}
|
|
74
|
+
/** The frame a CPU segment is charged to: the deepest component, else the deepest named app
|
|
75
|
+
* function, else the deepest dependency, else the runtime bucket the leaf is in. */
|
|
76
|
+
function owner_of_stack(frames) {
|
|
77
|
+
const named = (f) => f.name && f.name !== "(anonymous)" && !f.name.startsWith("(");
|
|
78
|
+
const file_of = (f) => f.url ? `${short(f.url)}:${f.line}` : "";
|
|
79
|
+
const leaf = frames.find((f) => named(f) && f.category !== "v8" && f.category !== "gc");
|
|
80
|
+
const pick = (f, label) => ({
|
|
81
|
+
label,
|
|
82
|
+
file: file_of(f),
|
|
83
|
+
category: f.category,
|
|
84
|
+
detail: leaf && leaf !== f ? leaf.name + (leaf.pkg ? ` (${leaf.pkg})` : "") : void 0
|
|
85
|
+
});
|
|
86
|
+
let comp;
|
|
87
|
+
let app;
|
|
88
|
+
let dep;
|
|
89
|
+
for (const f of frames) {
|
|
90
|
+
if (!comp && f.category === "component") comp = f;
|
|
91
|
+
if (!app && f.category === "app" && named(f)) app = f;
|
|
92
|
+
if (!dep && f.category === "dependency" && named(f)) dep = f;
|
|
93
|
+
}
|
|
94
|
+
if (comp) return pick(comp, comp.name);
|
|
95
|
+
if (app) return pick(app, app.name);
|
|
96
|
+
if (dep) return pick(dep, dep.pkg ? `${dep.name} (${dep.pkg})` : dep.name);
|
|
97
|
+
const anon = frames.find((f) => f.category === "app" && f.url);
|
|
98
|
+
if (anon) return pick(anon, anon.line <= 1 ? `module load (${short(anon.url)})` : `fn @ ${short(anon.url)}:${anon.line}`);
|
|
99
|
+
const anon_dep = frames.find((f) => f.category === "dependency" && f.pkg);
|
|
100
|
+
if (anon_dep) return pick(anon_dep, `${anon_dep.line <= 1 ? "module load" : "(anonymous)"} (${anon_dep.pkg})`);
|
|
101
|
+
const top = frames[0];
|
|
102
|
+
if (!top) return {
|
|
103
|
+
label: "unknown",
|
|
104
|
+
file: "",
|
|
105
|
+
category: "unknown"
|
|
106
|
+
};
|
|
107
|
+
if (top.category === "gc") return {
|
|
108
|
+
label: "garbage collection",
|
|
109
|
+
file: "",
|
|
110
|
+
category: "gc"
|
|
111
|
+
};
|
|
112
|
+
if (top.category === "svelte") return pick(top, "svelte internals");
|
|
113
|
+
if (top.category === "node") return pick(top, `node core: ${top.name || "(native)"}`);
|
|
114
|
+
if (top.category === "profiler") return pick(top, "profiler overhead");
|
|
115
|
+
return pick(top, top.name || "v8");
|
|
116
|
+
}
|
|
117
|
+
function short(url) {
|
|
118
|
+
const nm = url.lastIndexOf("node_modules/");
|
|
119
|
+
if (nm !== -1) return url.slice(nm + 13);
|
|
120
|
+
const parts = url.split("/");
|
|
121
|
+
return parts.length > 4 ? parts.slice(-4).join("/") : url;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build the timeline. `frame_of` resolves a profile node to its frame facts, `parent_of` the
|
|
125
|
+
* tree; both come from `analyze`'s own tables so the timeline agrees with every other view.
|
|
126
|
+
*/
|
|
127
|
+
function build_timeline(profile, frame_of, parent_of, input) {
|
|
128
|
+
const { start: w0, end: w1 } = input.window;
|
|
129
|
+
const window_ms = Math.max(w1 - w0, .01);
|
|
130
|
+
const stack_memo = /* @__PURE__ */ new Map();
|
|
131
|
+
const describe = (id) => {
|
|
132
|
+
const hit = stack_memo.get(id);
|
|
133
|
+
if (hit) return hit;
|
|
134
|
+
const frames = [];
|
|
135
|
+
let cur = id;
|
|
136
|
+
while (cur !== void 0 && frames.length < 64) {
|
|
137
|
+
frames.push(frame_of(cur));
|
|
138
|
+
cur = parent_of(cur);
|
|
139
|
+
}
|
|
140
|
+
const leaf = frames[0];
|
|
141
|
+
const idle = !!leaf && (leaf.category === "idle" || leaf.name === "(root)" || leaf.name === "(program)");
|
|
142
|
+
let lane = null;
|
|
143
|
+
let in_parent = false;
|
|
144
|
+
/** packages app code calls into on this stack, and the outermost package when no app code is on it */
|
|
145
|
+
const entered = [];
|
|
146
|
+
let root_pkg;
|
|
147
|
+
let has_app = false;
|
|
148
|
+
let inner;
|
|
149
|
+
for (let k = 0; k < frames.length; k++) {
|
|
150
|
+
const f = frames[k];
|
|
151
|
+
if (!lane && f.url && APP_LOAD_FILE_RE.test(f.url)) lane = load_lane_of(f.url);
|
|
152
|
+
if (f.name === "parent" && KIT_LOAD_RE.test(f.url)) in_parent = true;
|
|
153
|
+
if (f.category === "app" || f.category === "component") {
|
|
154
|
+
if (!has_app) inner = f;
|
|
155
|
+
has_app = true;
|
|
156
|
+
const child = frames[k - 1];
|
|
157
|
+
if (child?.category === "dependency" && child.pkg && !entered.includes(child.pkg)) entered.push(child.pkg);
|
|
158
|
+
} else if (f.category === "dependency" && f.pkg) root_pkg = f.pkg;
|
|
159
|
+
}
|
|
160
|
+
const d = {
|
|
161
|
+
owner: owner_of_stack(frames),
|
|
162
|
+
phase: phase_of_stack(frames, input.hooks),
|
|
163
|
+
idle,
|
|
164
|
+
lane,
|
|
165
|
+
in_parent,
|
|
166
|
+
entered,
|
|
167
|
+
orphan_pkg: has_app ? void 0 : root_pkg,
|
|
168
|
+
inner
|
|
169
|
+
};
|
|
170
|
+
stack_memo.set(id, d);
|
|
171
|
+
return d;
|
|
172
|
+
};
|
|
173
|
+
const cpu = [];
|
|
174
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
175
|
+
/** a lane's wait intervals, unioned at the end (parallel calls overlap) */
|
|
176
|
+
const lane_waits = /* @__PURE__ */ new Map();
|
|
177
|
+
const lane_row = (l) => {
|
|
178
|
+
let r = lanes.get(l.file);
|
|
179
|
+
if (!r) lanes.set(l.file, r = {
|
|
180
|
+
...l,
|
|
181
|
+
t0: Infinity,
|
|
182
|
+
t1: -Infinity,
|
|
183
|
+
cpu_ms: 0,
|
|
184
|
+
wait_ms: 0,
|
|
185
|
+
awaited_parent: false
|
|
186
|
+
});
|
|
187
|
+
return r;
|
|
188
|
+
};
|
|
189
|
+
const samples = profile.samples ?? [];
|
|
190
|
+
const deltas = profile.timeDeltas ?? [];
|
|
191
|
+
let t_us = profile.startTime;
|
|
192
|
+
/** package → the phase of the code that last called into it. A library that goes on working after
|
|
193
|
+
* an await (its own promise chain, a timer loop) has no app frame under it; that work belongs to
|
|
194
|
+
* whoever last called in — the same rule the functions table uses for a library's async work */
|
|
195
|
+
const entry_phase = /* @__PURE__ */ new Map();
|
|
196
|
+
/** owner label → `name\0file:line` of the deepest app function → ms */
|
|
197
|
+
const inner_ms = /* @__PURE__ */ new Map();
|
|
198
|
+
for (let i = 0; i < samples.length; i++) {
|
|
199
|
+
const d = deltas[i] ?? 0;
|
|
200
|
+
t_us += d;
|
|
201
|
+
const t = input.perf_start + (t_us - profile.startTime) / 1e3;
|
|
202
|
+
if (t > w1 || d <= 0) continue;
|
|
203
|
+
const described = describe(samples[i]);
|
|
204
|
+
if (described.idle) continue;
|
|
205
|
+
for (const pkg of described.entered) entry_phase.set(pkg, described.phase);
|
|
206
|
+
if (t < w0) continue;
|
|
207
|
+
const inherited = described.phase === "other" && described.orphan_pkg ? entry_phase.get(described.orphan_pkg) : void 0;
|
|
208
|
+
const info = inherited ? {
|
|
209
|
+
...described,
|
|
210
|
+
phase: inherited
|
|
211
|
+
} : described;
|
|
212
|
+
const s0 = Math.max(t - d / 1e3, w0) - w0;
|
|
213
|
+
const s1 = t - w0;
|
|
214
|
+
const o = info.owner;
|
|
215
|
+
if (info.inner && (o.category === "app" || o.category === "component")) {
|
|
216
|
+
const m = inner_ms.get(o.label) ?? inner_ms.set(o.label, /* @__PURE__ */ new Map()).get(o.label);
|
|
217
|
+
const k = `${info.inner.name}\0${short(info.inner.url)}:${info.inner.line}`;
|
|
218
|
+
m.set(k, (m.get(k) ?? 0) + s1 - s0);
|
|
219
|
+
}
|
|
220
|
+
if (info.lane) {
|
|
221
|
+
const row = lane_row(info.lane);
|
|
222
|
+
row.t0 = Math.min(row.t0, s0);
|
|
223
|
+
row.t1 = Math.max(row.t1, s1);
|
|
224
|
+
if (info.owner.category !== "profiler") row.cpu_ms += s1 - s0;
|
|
225
|
+
if (info.in_parent) row.awaited_parent = true;
|
|
226
|
+
}
|
|
227
|
+
const last = cpu[cpu.length - 1];
|
|
228
|
+
if (last && last.owner.label === info.owner.label && last.phase === info.phase && last.lane === info.lane?.file && s0 - last.t1 <= d / 1e3 * 2) last.t1 = s1;
|
|
229
|
+
else cpu.push({
|
|
230
|
+
t0: s0,
|
|
231
|
+
t1: s1,
|
|
232
|
+
owner: info.owner,
|
|
233
|
+
phase: info.phase,
|
|
234
|
+
...info.lane ? { lane: info.lane.file } : {}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const waits = [];
|
|
238
|
+
for (const c of input.calls) {
|
|
239
|
+
if (c.ms < 0) continue;
|
|
240
|
+
const a = Math.max(c.start, w0) - w0;
|
|
241
|
+
const b = Math.min(c.start + c.ms, w1) - w0;
|
|
242
|
+
if (b - a < .05) continue;
|
|
243
|
+
waits.push({
|
|
244
|
+
t0: a,
|
|
245
|
+
t1: b,
|
|
246
|
+
call: c
|
|
247
|
+
});
|
|
248
|
+
if (c.caller) {
|
|
249
|
+
const l = load_lane_of(c.caller);
|
|
250
|
+
if (l) {
|
|
251
|
+
const row = lane_row(l);
|
|
252
|
+
row.t0 = Math.min(row.t0, a);
|
|
253
|
+
row.t1 = Math.max(row.t1, b);
|
|
254
|
+
(lane_waits.get(l.file) ?? lane_waits.set(l.file, []).get(l.file)).push([a, b]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const bounds = /* @__PURE__ */ new Set([0, window_ms]);
|
|
259
|
+
for (const c of cpu) {
|
|
260
|
+
bounds.add(c.t0);
|
|
261
|
+
bounds.add(c.t1);
|
|
262
|
+
}
|
|
263
|
+
for (const w of waits) {
|
|
264
|
+
bounds.add(w.t0);
|
|
265
|
+
bounds.add(w.t1);
|
|
266
|
+
}
|
|
267
|
+
for (const p of input.pending ?? []) {
|
|
268
|
+
const a = p.start - w0;
|
|
269
|
+
const b = p.end - w0;
|
|
270
|
+
if (a > 0 && a < window_ms) bounds.add(a);
|
|
271
|
+
if (b > 0 && b < window_ms) bounds.add(b);
|
|
272
|
+
}
|
|
273
|
+
const ticks = [...bounds].sort((a, b) => a - b);
|
|
274
|
+
const segments = [];
|
|
275
|
+
let last_phase = "other";
|
|
276
|
+
let ci = 0;
|
|
277
|
+
for (let i = 0; i + 1 < ticks.length; i++) {
|
|
278
|
+
const a = ticks[i];
|
|
279
|
+
const b = ticks[i + 1];
|
|
280
|
+
if (b - a <= 0) continue;
|
|
281
|
+
const mid = (a + b) / 2;
|
|
282
|
+
while (ci < cpu.length && cpu[ci].t1 <= a) ci++;
|
|
283
|
+
const cpu_here = ci < cpu.length && cpu[ci].t0 <= mid && mid < cpu[ci].t1 ? cpu[ci] : null;
|
|
284
|
+
const all_active = waits.filter((w) => w.t0 <= mid && mid < w.t1);
|
|
285
|
+
const on_cpu = cpu_here && !(cpu_here.owner.category === "profiler" && all_active.length) ? cpu_here : null;
|
|
286
|
+
const open_spans = all_active.filter((w) => w.call.kind === "span");
|
|
287
|
+
const inner = open_spans.length ? open_spans.reduce((m, w) => w.call.ms < m.call.ms ? w : m, open_spans[0]) : null;
|
|
288
|
+
let seg;
|
|
289
|
+
if (on_cpu) {
|
|
290
|
+
last_phase = on_cpu.phase;
|
|
291
|
+
seg = {
|
|
292
|
+
t0: a,
|
|
293
|
+
t1: b,
|
|
294
|
+
kind: "cpu",
|
|
295
|
+
label: on_cpu.owner.label,
|
|
296
|
+
detail: on_cpu.owner.detail,
|
|
297
|
+
file: on_cpu.owner.file,
|
|
298
|
+
category: on_cpu.owner.category,
|
|
299
|
+
phase: on_cpu.phase,
|
|
300
|
+
...on_cpu.lane ? { lane: on_cpu.lane } : {},
|
|
301
|
+
...inner ? { within: inner.call.label } : {}
|
|
302
|
+
};
|
|
303
|
+
} else {
|
|
304
|
+
const active = all_active.filter((w) => w.call.kind !== "span");
|
|
305
|
+
if (active.length) {
|
|
306
|
+
const calls = active.map((w) => ({
|
|
307
|
+
label: w.call.label,
|
|
308
|
+
ms: round2(w.call.ms),
|
|
309
|
+
caller: w.call.caller,
|
|
310
|
+
...w.call.callers && w.call.callers.length > 1 ? { callers: w.call.callers } : {},
|
|
311
|
+
...w.call.timings ? { timings: w.call.timings } : {},
|
|
312
|
+
...w.call.body_ms ? { body_ms: w.call.body_ms } : {}
|
|
313
|
+
}));
|
|
314
|
+
const owner = active.reduce((m, w) => w.call.ms > m.call.ms ? w : m, active[0]);
|
|
315
|
+
seg = {
|
|
316
|
+
t0: a,
|
|
317
|
+
t1: b,
|
|
318
|
+
kind: "wait",
|
|
319
|
+
label: calls.length === 1 ? calls[0].label : `${calls.length} calls in parallel`,
|
|
320
|
+
category: "idle",
|
|
321
|
+
phase: owner.call.phase ?? inner?.call.phase ?? last_phase,
|
|
322
|
+
calls,
|
|
323
|
+
...inner ? { within: inner.call.label } : {}
|
|
324
|
+
};
|
|
325
|
+
} else if (inner) seg = {
|
|
326
|
+
t0: a,
|
|
327
|
+
t1: b,
|
|
328
|
+
kind: "wait",
|
|
329
|
+
label: `in ${inner.call.label}`,
|
|
330
|
+
category: "idle",
|
|
331
|
+
phase: inner.call.phase ?? last_phase,
|
|
332
|
+
calls: [{
|
|
333
|
+
label: inner.call.label,
|
|
334
|
+
ms: round2(inner.call.ms),
|
|
335
|
+
caller: inner.call.caller
|
|
336
|
+
}],
|
|
337
|
+
within: inner.call.label
|
|
338
|
+
};
|
|
339
|
+
else {
|
|
340
|
+
const pend = (input.pending ?? []).filter((p) => p.start - w0 <= mid && p.end - w0 >= mid).map((p) => p.label);
|
|
341
|
+
const uniq = [...new Set(pend)];
|
|
342
|
+
seg = {
|
|
343
|
+
t0: a,
|
|
344
|
+
t1: b,
|
|
345
|
+
kind: "gap",
|
|
346
|
+
label: uniq.length ? `nothing recorded — pending: ${uniq[0]}${uniq.length > 1 ? ` (+${uniq.length - 1})` : ""}` : "nothing recorded",
|
|
347
|
+
category: "unknown",
|
|
348
|
+
phase: last_phase,
|
|
349
|
+
...uniq.length ? { pending: uniq.slice(0, 6) } : {}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const prev = segments[segments.length - 1];
|
|
354
|
+
if (prev && prev.kind === seg.kind && prev.label === seg.label && prev.phase === seg.phase && prev.detail === seg.detail && prev.within === seg.within && prev.lane === seg.lane && (prev.pending?.join() ?? "") === (seg.pending?.join() ?? "") && call_id(prev) === call_id(seg)) prev.t1 = b;
|
|
355
|
+
else segments.push(seg);
|
|
356
|
+
}
|
|
357
|
+
for (const s of segments) {
|
|
358
|
+
s.t0 = round2(s.t0);
|
|
359
|
+
s.t1 = round2(s.t1);
|
|
360
|
+
}
|
|
361
|
+
for (let i = segments.length - 1; i >= 0; i--) if (segments[i].t1 <= segments[i].t0 && segments.length > 1) segments.splice(i, 1);
|
|
362
|
+
let cpu_ms = 0, wait_ms = 0, gap_ms = 0;
|
|
363
|
+
const phase_map = /* @__PURE__ */ new Map();
|
|
364
|
+
const phase_row = (p) => {
|
|
365
|
+
let r = phase_map.get(p);
|
|
366
|
+
if (!r) phase_map.set(p, r = {
|
|
367
|
+
phase: p,
|
|
368
|
+
cpu_ms: 0,
|
|
369
|
+
wait_ms: 0
|
|
370
|
+
});
|
|
371
|
+
return r;
|
|
372
|
+
};
|
|
373
|
+
/** phase → owner label → CPU ms: who a phase's CPU was */
|
|
374
|
+
const phase_owners = /* @__PURE__ */ new Map();
|
|
375
|
+
let overhead_ms = 0;
|
|
376
|
+
for (const s of segments) {
|
|
377
|
+
const len = s.t1 - s.t0;
|
|
378
|
+
if (s.kind === "cpu" && s.category === "profiler") overhead_ms += len;
|
|
379
|
+
else if (s.kind === "cpu") {
|
|
380
|
+
cpu_ms += len;
|
|
381
|
+
phase_row(s.phase).cpu_ms += len;
|
|
382
|
+
let m = phase_owners.get(s.phase);
|
|
383
|
+
if (!m) phase_owners.set(s.phase, m = /* @__PURE__ */ new Map());
|
|
384
|
+
const open = s.label.lastIndexOf(" (");
|
|
385
|
+
const who = s.category === "dependency" && open !== -1 && s.label.endsWith(")") ? s.label.slice(open + 2, -1) : s.category === "node" ? "node core" : s.label;
|
|
386
|
+
m.set(who, (m.get(who) ?? 0) + len);
|
|
387
|
+
} else if (s.kind === "wait") {
|
|
388
|
+
wait_ms += len;
|
|
389
|
+
phase_row(s.phase).wait_ms += len;
|
|
390
|
+
} else gap_ms += len;
|
|
391
|
+
}
|
|
392
|
+
const phases = [...phase_map.values()].map((r) => {
|
|
393
|
+
const top = [...phase_owners.get(r.phase) ?? []].filter(([, ms]) => ms >= r.cpu_ms * .1 && ms >= .5).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([label, ms]) => ({
|
|
394
|
+
label,
|
|
395
|
+
ms: round2(ms)
|
|
396
|
+
}));
|
|
397
|
+
return {
|
|
398
|
+
phase: r.phase,
|
|
399
|
+
cpu_ms: round2(r.cpu_ms),
|
|
400
|
+
wait_ms: round2(r.wait_ms),
|
|
401
|
+
...top.length ? { top } : {}
|
|
402
|
+
};
|
|
403
|
+
}).sort((x, y) => y.cpu_ms + y.wait_ms - (x.cpu_ms + x.wait_ms));
|
|
404
|
+
const timeline = {
|
|
405
|
+
window_ms: round2(window_ms),
|
|
406
|
+
cpu_ms: round2(cpu_ms),
|
|
407
|
+
wait_ms: round2(wait_ms),
|
|
408
|
+
gap_ms: round2(gap_ms),
|
|
409
|
+
overhead_ms: round2(overhead_ms),
|
|
410
|
+
segments,
|
|
411
|
+
phases,
|
|
412
|
+
parallelizable: []
|
|
413
|
+
};
|
|
414
|
+
timeline.parallelizable = find_parallelizable(timeline);
|
|
415
|
+
timeline.awaits = await_graph(input, w0, w1);
|
|
416
|
+
const inner = {};
|
|
417
|
+
for (const [owner, m] of inner_ms) {
|
|
418
|
+
const list = [...m].map(([k, ms]) => {
|
|
419
|
+
const z = k.indexOf("\0");
|
|
420
|
+
return {
|
|
421
|
+
name: k.slice(0, z),
|
|
422
|
+
file: k.slice(z + 1),
|
|
423
|
+
ms: round2(ms)
|
|
424
|
+
};
|
|
425
|
+
}).filter((f) => f.ms > 0).sort((a, b) => b.ms - a.ms).slice(0, 12);
|
|
426
|
+
if (list.some((f) => f.name !== owner)) inner[owner] = list;
|
|
427
|
+
}
|
|
428
|
+
if (Object.keys(inner).length) timeline.inner = inner;
|
|
429
|
+
for (const [file, list] of lane_waits) {
|
|
430
|
+
list.sort((x, y) => x[0] - y[0]);
|
|
431
|
+
let total = 0;
|
|
432
|
+
let cur = null;
|
|
433
|
+
for (const [a, b] of list) if (cur && a <= cur[1]) cur[1] = Math.max(cur[1], b);
|
|
434
|
+
else {
|
|
435
|
+
if (cur) total += cur[1] - cur[0];
|
|
436
|
+
cur = [a, b];
|
|
437
|
+
}
|
|
438
|
+
if (cur) total += cur[1] - cur[0];
|
|
439
|
+
lanes.get(file).wait_ms = total;
|
|
440
|
+
}
|
|
441
|
+
const lane_list = [...lanes.values()].filter((l) => Number.isFinite(l.t0)).map((l) => ({
|
|
442
|
+
...l,
|
|
443
|
+
t0: round2(l.t0),
|
|
444
|
+
t1: round2(l.t1),
|
|
445
|
+
cpu_ms: round2(l.cpu_ms),
|
|
446
|
+
wait_ms: round2(l.wait_ms)
|
|
447
|
+
})).sort((x, y) => x.t0 - y.t0);
|
|
448
|
+
if (lane_list.length) {
|
|
449
|
+
timeline.lanes = lane_list;
|
|
450
|
+
const page = lane_list.find((l) => l.level === "page");
|
|
451
|
+
const layout = lane_list.filter((l) => l.level === "layout" && l.kind === page?.kind && l.t1 - l.t0 >= 2 && (l.wait_ms > 0 || l.cpu_ms >= 2)).sort((x, y) => y.t1 - x.t1)[0];
|
|
452
|
+
if (page && layout && (page.awaited_parent || page.t0 >= layout.t1 - .25)) timeline.chain = {
|
|
453
|
+
layout: layout.file,
|
|
454
|
+
page: page.file,
|
|
455
|
+
serial_ms: round2(Math.max(0, Math.min(layout.t1, page.t0) - layout.t0)),
|
|
456
|
+
explicit: page.awaited_parent
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
return timeline;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* THE CAUSALITY GRAPH: every call (net + I/O primitives, not spans) as a box on the render's
|
|
463
|
+
* clock, laid into lanes so overlapping calls sit on different rows, and an edge from a call to
|
|
464
|
+
* the one that started right after it finished while nothing else was in flight — that start
|
|
465
|
+
* waited for that end, and `at` is the line that did the waiting. Edges are what the
|
|
466
|
+
* "awaits in a row" callouts are made of; the graph shows them with the exact await site.
|
|
467
|
+
*/
|
|
468
|
+
function await_graph(input, w0, w1) {
|
|
469
|
+
const calls = input.calls.filter((c) => c.kind !== "span" && c.ms >= 0 && c.start <= w1 && c.start + c.ms >= w0).map((c) => ({
|
|
470
|
+
c,
|
|
471
|
+
t0: round2(Math.max(c.start, w0) - w0),
|
|
472
|
+
t1: round2(Math.min(c.start + c.ms, w1) - w0)
|
|
473
|
+
})).sort((x, y) => x.t0 - y.t0 || x.t1 - y.t1).slice(0, 200);
|
|
474
|
+
const lane_end = [];
|
|
475
|
+
const nodes = calls.map(({ c, t0, t1 }) => {
|
|
476
|
+
let lane = lane_end.findIndex((e) => e <= t0);
|
|
477
|
+
if (lane === -1) lane = lane_end.push(0) - 1;
|
|
478
|
+
lane_end[lane] = t1;
|
|
479
|
+
return {
|
|
480
|
+
label: c.label,
|
|
481
|
+
t0,
|
|
482
|
+
t1,
|
|
483
|
+
lane,
|
|
484
|
+
kind: c.kind,
|
|
485
|
+
...c.caller ? { caller: c.caller } : {}
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
const edges = [];
|
|
489
|
+
for (let i = 0; i < calls.length; i++) {
|
|
490
|
+
const cur = calls[i];
|
|
491
|
+
let prev = null;
|
|
492
|
+
for (let j = 0; j < i; j++) {
|
|
493
|
+
const p = calls[j];
|
|
494
|
+
if (p.t1 <= cur.t0 + .05 && cur.t0 - p.t1 <= 5 && (!prev || p.t1 > prev.t1)) prev = p;
|
|
495
|
+
}
|
|
496
|
+
if (!prev) continue;
|
|
497
|
+
if (calls.some((o, j) => j !== i && o !== prev && o.t0 < cur.t0 - .05 && o.t1 > cur.t0 + .05)) continue;
|
|
498
|
+
edges.push({
|
|
499
|
+
from: prev.c.label,
|
|
500
|
+
to: cur.c.label,
|
|
501
|
+
gap_ms: round2(Math.max(0, cur.t0 - prev.t1)),
|
|
502
|
+
...cur.c.caller ? { at: cur.c.caller } : {},
|
|
503
|
+
...cur.c.callers && cur.c.callers.length > 1 ? { callers: cur.c.callers } : {}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
nodes,
|
|
508
|
+
edges
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
/** A wait's call, told apart from another call of the same label by its whole duration (the label
|
|
512
|
+
* groups by route — `GET /api?name=a` and `?name=b` read the same). A segment that is not one
|
|
513
|
+
* call's wait is its label. */
|
|
514
|
+
function call_id(s) {
|
|
515
|
+
return s.kind === "wait" && s.calls?.length === 1 ? `${s.label}\0${s.calls[0].ms}` : s.label;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Awaits in a row: consecutive single-call waits on DIFFERENT calls with little CPU between them —
|
|
519
|
+
* 5 ms, or 5% of the chain's longest wait when that is more. Read off the COALESCED view
|
|
520
|
+
* (`coalesce`), where a wait chopped by microtask slivers is one block again — on the raw segments
|
|
521
|
+
* the same call reappearing after a sliver broke every chain. `at` indexes that view (the same one
|
|
522
|
+
* the UI draws).
|
|
523
|
+
*
|
|
524
|
+
* (The allowance scales: between a 1 s timer and a 700 ms call, the CPU of the server answering the
|
|
525
|
+
* page's own call to itself, or a collection, ran 3 to 9 ms run by run — the fixed 5 ms broke the
|
|
526
|
+
* chain in some runs and not others, and a 2.2 s finding came and went between two profiles of the
|
|
527
|
+
* same code. Next to waits that long, tens of ms of CPU are still nothing between.)
|
|
528
|
+
*/
|
|
529
|
+
function find_parallelizable(t) {
|
|
530
|
+
const view = coalesce(t);
|
|
531
|
+
const parallelizable = [];
|
|
532
|
+
let group = null;
|
|
533
|
+
const limit = () => group ? Math.max(5, Math.max(...group.lens) * .05) : 5;
|
|
534
|
+
const flush = () => {
|
|
535
|
+
if (group && group.at.length >= 2) {
|
|
536
|
+
const sum = group.lens.reduce((a, c) => a + c, 0);
|
|
537
|
+
const max = Math.max(...group.lens);
|
|
538
|
+
if (sum - max >= 1) parallelizable.push({
|
|
539
|
+
calls: group.labels,
|
|
540
|
+
ms: round2(sum),
|
|
541
|
+
save_ms: round2(sum - max),
|
|
542
|
+
at: group.at
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
group = null;
|
|
546
|
+
};
|
|
547
|
+
let cpu_between = 0;
|
|
548
|
+
for (let i = 0; i < view.length; i++) {
|
|
549
|
+
const s = view[i];
|
|
550
|
+
if (s.kind === "wait" && s.calls && s.calls.length === 1) {
|
|
551
|
+
const label = s.calls[0].label;
|
|
552
|
+
const id = call_id(s);
|
|
553
|
+
if (group && group.ids[group.ids.length - 1] === id && cpu_between <= limit()) {
|
|
554
|
+
group.lens[group.lens.length - 1] += s.t1 - s.t0;
|
|
555
|
+
cpu_between = 0;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (group && (cpu_between > limit() || group.ids.includes(id))) flush();
|
|
559
|
+
if (!group) group = {
|
|
560
|
+
at: [],
|
|
561
|
+
labels: [],
|
|
562
|
+
ids: [],
|
|
563
|
+
lens: []
|
|
564
|
+
};
|
|
565
|
+
group.at.push(i);
|
|
566
|
+
group.labels.push(label);
|
|
567
|
+
group.ids.push(id);
|
|
568
|
+
group.lens.push(s.t1 - s.t0);
|
|
569
|
+
cpu_between = 0;
|
|
570
|
+
} else if (s.kind === "cpu") {
|
|
571
|
+
cpu_between += s.t1 - s.t0;
|
|
572
|
+
if (cpu_between > limit()) flush();
|
|
573
|
+
} else if (s.kind === "wait") flush();
|
|
574
|
+
}
|
|
575
|
+
flush();
|
|
576
|
+
return parallelizable;
|
|
577
|
+
}
|
|
578
|
+
function coalesce(t, min_ms = Math.max(t.window_ms * .012, 1)) {
|
|
579
|
+
const out = [];
|
|
580
|
+
let small = [];
|
|
581
|
+
const len = (s) => s.t1 - s.t0;
|
|
582
|
+
const same = (a, b) => a.kind === b.kind && a.label === b.label && a.within === b.within && call_id(a) === call_id(b);
|
|
583
|
+
const fold = (list) => {
|
|
584
|
+
const by = /* @__PURE__ */ new Map();
|
|
585
|
+
const kind_ms = {
|
|
586
|
+
cpu: 0,
|
|
587
|
+
wait: 0,
|
|
588
|
+
gap: 0
|
|
589
|
+
};
|
|
590
|
+
for (const s of list) {
|
|
591
|
+
const k = s.kind + "\0" + s.label;
|
|
592
|
+
const e = by.get(k) ?? {
|
|
593
|
+
label: s.label,
|
|
594
|
+
kind: s.kind,
|
|
595
|
+
ms: 0,
|
|
596
|
+
seg: s
|
|
597
|
+
};
|
|
598
|
+
e.ms += len(s);
|
|
599
|
+
by.set(k, e);
|
|
600
|
+
kind_ms[s.kind] += len(s);
|
|
601
|
+
}
|
|
602
|
+
const inside = [...by.values()].sort((a, b) => b.ms - a.ms);
|
|
603
|
+
const kind = Object.entries(kind_ms).sort((a, b) => b[1] - a[1])[0][0];
|
|
604
|
+
const top = inside.find((i) => i.kind === kind) ?? inside[0];
|
|
605
|
+
const phases = /* @__PURE__ */ new Map();
|
|
606
|
+
for (const s of list) phases.set(s.phase, (phases.get(s.phase) ?? 0) + len(s));
|
|
607
|
+
const phase = [...phases.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
608
|
+
const withins = /* @__PURE__ */ new Map();
|
|
609
|
+
for (const s of list) withins.set(s.within, (withins.get(s.within) ?? 0) + len(s));
|
|
610
|
+
const within = [...withins.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
611
|
+
const others = inside.filter((i) => i.kind === kind).length - 1;
|
|
612
|
+
return {
|
|
613
|
+
t0: list[0].t0,
|
|
614
|
+
t1: list[list.length - 1].t1,
|
|
615
|
+
kind,
|
|
616
|
+
label: list.length === 1 ? top.label : `mostly ${top.label}${others > 0 ? ` (+${others} more)` : ""}`,
|
|
617
|
+
detail: list.length === 1 ? top.seg.detail : void 0,
|
|
618
|
+
file: top.seg.file,
|
|
619
|
+
category: top.seg.category,
|
|
620
|
+
phase,
|
|
621
|
+
calls: kind === "wait" ? top.seg.calls : void 0,
|
|
622
|
+
within,
|
|
623
|
+
parts: list.length,
|
|
624
|
+
inside: list.length === 1 ? void 0 : inside.map(({ label, kind, ms }) => ({
|
|
625
|
+
label,
|
|
626
|
+
kind,
|
|
627
|
+
ms: Math.round(ms * 100) / 100
|
|
628
|
+
}))
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
const flush_small = () => {
|
|
632
|
+
if (!small.length) return;
|
|
633
|
+
const total = small.reduce((s, x) => s + len(x), 0);
|
|
634
|
+
const last = out[out.length - 1];
|
|
635
|
+
if (last && total < min_ms && small.every((s) => s.kind !== "wait" || same(s, last))) {
|
|
636
|
+
last.t1 = small[small.length - 1].t1;
|
|
637
|
+
last.parts += small.length;
|
|
638
|
+
} else out.push(fold(small));
|
|
639
|
+
small = [];
|
|
640
|
+
};
|
|
641
|
+
for (const s of t.segments) if (len(s) >= min_ms) {
|
|
642
|
+
const last = out[out.length - 1];
|
|
643
|
+
if (last && same(s, last) && small.every((x) => len(x) < min_ms)) {
|
|
644
|
+
last.t1 = s.t1;
|
|
645
|
+
last.parts += small.length + 1;
|
|
646
|
+
small = [];
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const total = small.reduce((a, x) => a + len(x), 0);
|
|
650
|
+
if (small.length && !last && total < min_ms) {
|
|
651
|
+
out.push({
|
|
652
|
+
...s,
|
|
653
|
+
t0: small[0].t0,
|
|
654
|
+
parts: small.length + 1
|
|
655
|
+
});
|
|
656
|
+
small = [];
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
flush_small();
|
|
660
|
+
out.push({
|
|
661
|
+
...s,
|
|
662
|
+
parts: 1
|
|
663
|
+
});
|
|
664
|
+
} else small.push(s);
|
|
665
|
+
flush_small();
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
/** A net call's timeline label: `GET host/path` (query dropped, long paths cut). */
|
|
669
|
+
function label_call(method, url) {
|
|
670
|
+
try {
|
|
671
|
+
const u = new URL(url);
|
|
672
|
+
const p = u.pathname.length > 40 ? u.pathname.slice(0, 37) + "…" : u.pathname;
|
|
673
|
+
return `${method} ${u.host}${p}`;
|
|
674
|
+
} catch {
|
|
675
|
+
return `${method} ${url.length > 48 ? url.slice(0, 45) + "…" : url}`;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** The steps worth listing: every coalesced segment at or above `min_pct` of the window, in time
|
|
679
|
+
* order (`i` indexes the coalesced list). */
|
|
680
|
+
function chain_steps(t, min_pct = 2, max = 14) {
|
|
681
|
+
const rows = coalesce(t).map((seg, i) => ({
|
|
682
|
+
i,
|
|
683
|
+
seg,
|
|
684
|
+
ms: round2(seg.t1 - seg.t0),
|
|
685
|
+
pct: (seg.t1 - seg.t0) / t.window_ms * 100
|
|
686
|
+
})).filter((r) => r.pct >= min_pct);
|
|
687
|
+
if (rows.length <= max) return rows;
|
|
688
|
+
return rows.sort((a, b) => b.ms - a.ms).slice(0, max).sort((a, b) => a.i - b.i);
|
|
689
|
+
}
|
|
690
|
+
export { PHASE_LABEL, await_graph, build_timeline, chain_steps, coalesce, find_parallelizable, label_call, load_lane_of, owner_of_stack, phase_of_frame, phase_of_stack };
|