@ogygia/devtools 0.11.4 → 0.13.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/README.md +1 -1
- package/dist/app.d.ts +3 -0
- package/dist/app.js +9 -0
- package/dist/devtools/BoundaryOverlay.svelte +10 -6
- package/dist/devtools/Devtools.svelte +24 -18
- package/dist/devtools/IslandDetail.svelte +7 -5
- package/dist/devtools/PageTab.svelte +4 -2
- package/dist/devtools/ProfilerTab.svelte +8 -2
- package/dist/devtools/SessionTab.svelte +21 -8
- package/dist/devtools/highlight.js +14 -7
- package/dist/devtools/ledger-dom.js +6 -9
- package/dist/devtools/names.js +18 -17
- package/dist/devtools/page.d.ts +2 -0
- package/dist/devtools/page.js +37 -17
- package/dist/devtools/profile-store.d.ts +4 -9
- package/dist/devtools/profile-store.js +26 -21
- package/dist/devtools/session.d.ts +2 -0
- package/dist/devtools/session.js +48 -42
- package/dist/devtools/testing-engine.js +10 -1
- package/dist/devtools/ui-app.d.ts +3 -1
- package/dist/devtools/ui-app.js +9 -3
- package/dist/devtools/ui.js +83 -40
- package/dist/messages/build.d.ts +21 -0
- package/dist/messages/build.js +56 -0
- package/dist/messages/index.d.ts +3 -0
- package/dist/messages/index.js +3 -0
- package/dist/messages/runtime.d.ts +63 -0
- package/dist/messages/runtime.js +122 -0
- package/dist/profiler/async-io.d.ts +12 -7
- package/dist/profiler/async-io.js +94 -82
- package/dist/profiler/beacon-standalone.js +15 -6
- package/dist/profiler/busy.d.ts +13 -0
- package/dist/profiler/busy.js +45 -0
- package/dist/profiler/client.js +5 -24
- package/dist/profiler/embedded-maps.js +9 -5
- package/dist/profiler/gc.d.ts +5 -0
- package/dist/profiler/gc.js +75 -11
- package/dist/profiler/index.d.ts +23 -2
- package/dist/profiler/index.js +648 -373
- package/dist/profiler/net.d.ts +14 -6
- package/dist/profiler/net.js +61 -45
- package/dist/profiler/patterns.js +29 -1
- package/dist/profiler/source-scan.js +24 -12
- package/dist/profiler/standalone.js +12 -4
- package/dist/profiler/storage/index.d.ts +14 -3
- package/dist/profiler/storage/index.js +41 -41
- package/dist/profiler/storage/postgres.d.ts +18 -5
- package/dist/profiler/storage/postgres.js +124 -85
- package/dist/profiler/storage/redis.d.ts +18 -5
- package/dist/profiler/storage/redis.js +160 -131
- package/dist/profiler/storage/sqlite.d.ts +15 -2
- package/dist/profiler/storage/sqlite.js +54 -48
- package/dist/profiler/ui/ComponentsTable.svelte +2 -2
- package/dist/profiler/ui/Dashboard.svelte +2 -3
- package/dist/profiler/ui/DownloadHtml.svelte +1 -0
- package/dist/profiler/ui/ExportButton.svelte +1 -0
- package/dist/profiler/ui/Flame.svelte +10 -4
- package/dist/profiler/ui/Flame.svelte.d.ts +0 -5
- package/dist/profiler/ui/FrameDetails.svelte +13 -2
- package/dist/profiler/ui/FrameDetails.svelte.d.ts +0 -6
- package/dist/profiler/ui/FunctionsTable.svelte +2 -2
- package/dist/profiler/ui/IslandsTable.svelte +2 -2
- package/dist/profiler/ui/OnThisPage.svelte +12 -4
- package/dist/profiler/ui/RunView.svelte +31 -27
- package/dist/profiler/ui/ShareLink.svelte +6 -1
- package/dist/profiler/ui/Sidebar.svelte +14 -9
- package/dist/profiler/ui/Treemap.svelte +11 -4
- package/dist/profiler/ui/Treemap.svelte.d.ts +0 -6
- package/dist/profiler/ui/WaitingTable.svelte +2 -2
- package/dist/profiler/ui/fake-progress.svelte.d.ts +7 -12
- package/dist/profiler/ui/fake-progress.svelte.js +57 -36
- package/dist/profiler/ui/press.js +4 -2
- package/dist/profiler/ui/row-anchor.svelte.d.ts +0 -6
- package/dist/profiler/ui/row-anchor.svelte.js +13 -8
- package/dist/profiler/ui/sort.svelte.d.ts +9 -6
- package/dist/profiler/ui/sort.svelte.js +34 -28
- package/dist/profiler/visit.d.ts +0 -1
- package/dist/profiler/visit.js +1 -2
- package/dist/server.js +27 -10
- package/dist/vite.js +3 -2
- package/package.json +18 -9
package/README.md
CHANGED
|
@@ -23,4 +23,4 @@ export default defineConfig(({ mode }) => ({
|
|
|
23
23
|
|
|
24
24
|
Code for the profiler: `@ogygia/devtools/profiler` (`span`, `instrument`), `@ogygia/devtools/profiler/client` (`mark`), and stores at `@ogygia/devtools/profiler/storage/sqlite`, `/redis`, `/postgres`.
|
|
25
25
|
|
|
26
|
-
Docs: [
|
|
26
|
+
Docs: [@ogygia/devtools](https://ogygiajs.dev/docs/devtools/overview), with [the dock](https://ogygiajs.dev/docs/devtools/dock) and [the profiler](https://ogygiajs.dev/docs/devtools/profiler/overview).
|
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* the real element under the cursor via `<svelte:window>`. A pure consumer, drawn only while on.
|
|
16
16
|
*/
|
|
17
17
|
import { onMount } from 'svelte';
|
|
18
|
+
import { scope } from '@ogygia/machine';
|
|
18
19
|
import {
|
|
19
20
|
all_regions,
|
|
20
21
|
region_info,
|
|
@@ -45,19 +46,22 @@
|
|
|
45
46
|
const onkey = (e) => {
|
|
46
47
|
if (e.key === 'Escape' && picking) picking = false;
|
|
47
48
|
};
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
const s = scope();
|
|
50
|
+
s.on(window, 'keydown', onkey);
|
|
51
|
+
return () => s.close();
|
|
50
52
|
});
|
|
51
53
|
// The rAF loop runs ONLY while the overlay is on (it follows the `overlay` prop, hence an effect):
|
|
52
54
|
// a loop left running with the overlay off asked the browser for a frame sixty times a second on
|
|
53
55
|
// every page with devtools, the dock closed — ~90 ms of main-thread work each 5 s of idle
|
|
54
56
|
$effect(() => {
|
|
55
57
|
if (!overlay) return;
|
|
56
|
-
|
|
58
|
+
const s = scope();
|
|
59
|
+
const loop = () => {
|
|
57
60
|
frame++;
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
+
s.timeout(loop, 'frame');
|
|
62
|
+
};
|
|
63
|
+
s.timeout(loop, 'frame');
|
|
64
|
+
return () => s.close();
|
|
61
65
|
});
|
|
62
66
|
|
|
63
67
|
const boxes = $derived.by(() => {
|
|
@@ -29,7 +29,8 @@ shadow boundary keeps it from the dock — a built dock opened unstyled. -->
|
|
|
29
29
|
import PageTab from './PageTab.svelte';
|
|
30
30
|
import SessionTab from './SessionTab.svelte';
|
|
31
31
|
import HydrationTab from './HydrationTab.svelte';
|
|
32
|
-
import { onMount } from 'svelte';
|
|
32
|
+
import { onDestroy, onMount } from 'svelte';
|
|
33
|
+
import { scope, STALE } from '@ogygia/machine';
|
|
33
34
|
import BoundaryOverlay from './BoundaryOverlay.svelte';
|
|
34
35
|
import IslandDetail from './IslandDetail.svelte';
|
|
35
36
|
import { to_trace } from '@ogygia/core/events';
|
|
@@ -93,7 +94,7 @@ shadow boundary keeps it from the dock — a built dock opened unstyled. -->
|
|
|
93
94
|
JSON.stringify({ open, launch: launchPos, win: winPos, rz: winResizeOff, size: winSize })
|
|
94
95
|
);
|
|
95
96
|
} catch {
|
|
96
|
-
|
|
97
|
+
// ok: private mode or quota: the layout is not kept
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -155,15 +156,19 @@ shadow boundary keeps it from the dock — a built dock opened unstyled. -->
|
|
|
155
156
|
// Explicit hover state for the launcher (pointerenter/leave — a `:hover` descendant selector proved
|
|
156
157
|
// flaky against neodrag's inline transform). `up` = fully revealed; otherwise it peeks below the fold.
|
|
157
158
|
let launchHover = $state(false);
|
|
158
|
-
|
|
159
|
+
/** The dock's own work in the page (its keys, its timers, its fetch): let go when it unmounts. */
|
|
160
|
+
const owner = scope();
|
|
161
|
+
onDestroy(() => owner.close());
|
|
162
|
+
/** The live refresh while the window is open: a child of the dock's scope. */
|
|
163
|
+
let refresh = /** @type {import('@ogygia/machine').Scope | null} */ (null);
|
|
159
164
|
function refresh_on() {
|
|
160
|
-
if (
|
|
165
|
+
if (refresh) return;
|
|
166
|
+
refresh = owner.child();
|
|
167
|
+
refresh.every(() => tick++, 700);
|
|
161
168
|
}
|
|
162
169
|
function refresh_off() {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
tickTimer = null;
|
|
166
|
-
}
|
|
170
|
+
refresh?.close();
|
|
171
|
+
refresh = null;
|
|
167
172
|
}
|
|
168
173
|
function place_near_launcher() {
|
|
169
174
|
winPos = {
|
|
@@ -216,10 +221,12 @@ shadow boundary keeps it from the dock — a built dock opened unstyled. -->
|
|
|
216
221
|
a.href = url;
|
|
217
222
|
a.download = `ogygia-trace-${Date.now()}.json`;
|
|
218
223
|
a.click();
|
|
224
|
+
// (the download is the page's, not the dock's: it lets go of the file even if the dock goes)
|
|
225
|
+
// oxlint-disable-next-line ogygia/owned-effects -- one-shot, ends itself, outlives the dock on purpose
|
|
219
226
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
220
227
|
copied = 'saved as a file';
|
|
221
228
|
}
|
|
222
|
-
|
|
229
|
+
owner.timeout(() => (copied = ''), 1600);
|
|
223
230
|
}
|
|
224
231
|
|
|
225
232
|
// One-time setup: Alt+O toggle, the compiler's devtools metadata, and (re)starting the refresh timer
|
|
@@ -231,28 +238,27 @@ shadow boundary keeps it from the dock — a built dock opened unstyled. -->
|
|
|
231
238
|
toggle_open();
|
|
232
239
|
}
|
|
233
240
|
};
|
|
234
|
-
|
|
241
|
+
owner.on(window, 'keydown', onkey);
|
|
235
242
|
|
|
236
243
|
// `names` (island id → component name) and `bytes` (island id → transitive dev-graph size) label
|
|
237
244
|
// the tabs with real names/costs. Best-effort — the endpoint only exists on the dev server (a
|
|
238
245
|
// build carries its names in the dock's own module: ui-app.ts).
|
|
239
246
|
if (!LAZY)
|
|
240
|
-
|
|
241
|
-
.
|
|
247
|
+
owner
|
|
248
|
+
.task((signal) =>
|
|
249
|
+
fetch('/__ogygia_devtools_meta', { signal }).then((r) => (r.ok ? r.json() : null))
|
|
250
|
+
)
|
|
242
251
|
.then((meta) => {
|
|
243
|
-
|
|
252
|
+
// (STALE: the dock unmounted first)
|
|
253
|
+
if (!meta || meta === STALE || typeof meta !== 'object') return;
|
|
244
254
|
if (meta.names) window.__ogygia_region_names = meta.names;
|
|
245
255
|
if (meta.bytes) window.__ogygia_region_bytes = meta.bytes;
|
|
246
256
|
tick++;
|
|
247
257
|
})
|
|
248
|
-
.catch(() => {});
|
|
258
|
+
.catch(() => {}); // ok: best effort (dev server only): without names the tabs show hashes
|
|
249
259
|
|
|
250
260
|
if (startOpen && !open) set_open(true);
|
|
251
261
|
else if (open) refresh_on();
|
|
252
|
-
return () => {
|
|
253
|
-
window.removeEventListener('keydown', onkey);
|
|
254
|
-
refresh_off();
|
|
255
|
-
};
|
|
256
262
|
});
|
|
257
263
|
</script>
|
|
258
264
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { parse } from 'devalue';
|
|
14
14
|
import { onDestroy } from 'svelte';
|
|
15
|
+
import { scope } from '@ogygia/machine';
|
|
15
16
|
import { highlight, clear_highlight } from './highlight.js';
|
|
16
17
|
import {
|
|
17
18
|
region_info,
|
|
@@ -142,14 +143,15 @@
|
|
|
142
143
|
});
|
|
143
144
|
|
|
144
145
|
// scroll to it AND mark it for a moment: an island already on screen gave no sign at all
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
const owner = scope();
|
|
147
|
+
onDestroy(() => owner.close());
|
|
148
|
+
/** @type {(() => void) | null} */
|
|
149
|
+
let unflash = null;
|
|
147
150
|
function locate() {
|
|
148
151
|
highlight([el], model.name, true);
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
unflash?.();
|
|
153
|
+
unflash = owner.timeout(clear_highlight, 1400);
|
|
151
154
|
}
|
|
152
|
-
onDestroy(() => clearTimeout(flash));
|
|
153
155
|
</script>
|
|
154
156
|
|
|
155
157
|
<div class="detail" data-og-detail>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* waterfall of every island's wake (module → turn → hydrate, against FCP / LCP / DCL / load), the
|
|
10
10
|
* files that blocked the first paint, and the bytes by type. The analysis is page-insights.ts.
|
|
11
11
|
*/
|
|
12
|
+
import { scope } from '@ogygia/machine';
|
|
12
13
|
import { read_page, cpu_of, summary_lines } from './page.js';
|
|
13
14
|
import { first_difference, rate } from '@ogygia/core/insights';
|
|
14
15
|
import { beacon_record_cpu } from '@ogygia/core/internal/instruments';
|
|
@@ -48,14 +49,15 @@
|
|
|
48
49
|
const nav_key = $derived(view?.nav?.t ?? 0);
|
|
49
50
|
$effect(() => {
|
|
50
51
|
void nav_key;
|
|
51
|
-
const
|
|
52
|
+
const s = scope();
|
|
53
|
+
s.timeout(
|
|
52
54
|
() => {
|
|
53
55
|
check_styles();
|
|
54
56
|
check_cache();
|
|
55
57
|
},
|
|
56
58
|
nav_key ? 400 : 60
|
|
57
59
|
);
|
|
58
|
-
return () =>
|
|
60
|
+
return () => s.close();
|
|
59
61
|
});
|
|
60
62
|
const findings = $derived.by(() => {
|
|
61
63
|
if (!view) return [];
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* the rest of devtools, so the profiler answers without a key.
|
|
13
13
|
*/
|
|
14
14
|
import { onMount } from 'svelte';
|
|
15
|
+
import { scope } from '@ogygia/machine';
|
|
15
16
|
import { run_profile, profile_for, profiles_version } from './profile-store.js';
|
|
16
17
|
import { page_anchors } from './profile-anchors.js';
|
|
17
18
|
import { highlight, clear_highlight } from './highlight.js';
|
|
@@ -27,13 +28,18 @@
|
|
|
27
28
|
let error = $state('');
|
|
28
29
|
let bump = $state(0); // a new profile landed
|
|
29
30
|
|
|
31
|
+
/** The tab's profiles (their requests, their waits for a busy profiler): end with the tab. */
|
|
32
|
+
const owner = scope();
|
|
30
33
|
onMount(() => {
|
|
31
34
|
path = location.pathname;
|
|
32
35
|
// Probe: a mounted profiler answers its base (200/redirect); an un-mounted one 404s.
|
|
33
36
|
fetch(BASE, { method: 'GET', redirect: 'manual' })
|
|
34
37
|
.then((r) => (available = r.status !== 404))
|
|
35
38
|
.catch(() => (available = false));
|
|
36
|
-
return () =>
|
|
39
|
+
return () => {
|
|
40
|
+
clear_highlight(); // leaving the tab takes the boxes off the page
|
|
41
|
+
owner.close();
|
|
42
|
+
};
|
|
37
43
|
});
|
|
38
44
|
|
|
39
45
|
const prof = $derived.by(() => {
|
|
@@ -57,7 +63,7 @@
|
|
|
57
63
|
started = performance.now();
|
|
58
64
|
const p = path.startsWith('/') ? path : '/' + path;
|
|
59
65
|
try {
|
|
60
|
-
await run_profile(p, runs, BASE);
|
|
66
|
+
if ((await run_profile(p, runs, BASE, owner)) === null) return; // the tab was left
|
|
61
67
|
bump++;
|
|
62
68
|
} catch (e) {
|
|
63
69
|
error = `The profile failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* Engine: session.ts; analysis: session-insights.ts.
|
|
21
21
|
*/
|
|
22
22
|
import { onMount } from 'svelte';
|
|
23
|
+
import { flights, scope } from '@ogygia/machine';
|
|
23
24
|
import { start_session, recording, set_last_session, session_names } from './session.js';
|
|
24
25
|
import { run_profile, slim_profile } from './profile-store.js';
|
|
25
26
|
import { analyze_session, session_timeline } from '@ogygia/core/insights';
|
|
@@ -51,13 +52,19 @@
|
|
|
51
52
|
let server_window = $state(
|
|
52
53
|
/** @type {import('./profile-store.js').SlimProfile & { window?: any } | null} */ (null)
|
|
53
54
|
);
|
|
54
|
-
/**
|
|
55
|
-
|
|
55
|
+
/** Is the profiler here? Asked once and kept; a probe the network failed is not kept (the next
|
|
56
|
+
* ask probes again), and reads as no for now. */
|
|
57
|
+
const probe = flights({
|
|
58
|
+
name: 'devtools profiler probe',
|
|
59
|
+
run: () =>
|
|
60
|
+
fetch('/__profiler', { redirect: 'manual' }).then((r) => (profiler_here = r.status !== 404))
|
|
61
|
+
});
|
|
56
62
|
const probe_profiler = () =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
probe.get().then(
|
|
64
|
+
(here) => here === true,
|
|
65
|
+
() => (profiler_here = false)
|
|
66
|
+
);
|
|
67
|
+
onMount(() => void probe_profiler()); // ok: never rejects (a failed probe reads as no profiler)
|
|
61
68
|
let window_on = false;
|
|
62
69
|
/** the window's start in flight (a stop pressed before it answered waits, then stops it) */
|
|
63
70
|
let window_starting = /** @type {Promise<void> | null} */ (null);
|
|
@@ -174,12 +181,15 @@
|
|
|
174
181
|
let server = $state(
|
|
175
182
|
/** @type {Record<string, { busy?: boolean; error?: string; p?: import('./profile-store.js').SlimProfile }>} */ ({})
|
|
176
183
|
);
|
|
184
|
+
/** The tab's server profiles (their requests, their waits for a busy profiler): end with the tab. */
|
|
185
|
+
const owner = scope();
|
|
177
186
|
async function profile_request(url) {
|
|
178
187
|
const u = new URL(url);
|
|
179
188
|
const path = u.pathname + u.search;
|
|
180
189
|
server = { ...server, [url]: { busy: true } };
|
|
181
190
|
try {
|
|
182
|
-
const p = await run_profile(path, 3);
|
|
191
|
+
const p = await run_profile(path, 3, '/__profiler', owner);
|
|
192
|
+
if (p === null) return; // the tab was left
|
|
183
193
|
server = { ...server, [url]: { p } };
|
|
184
194
|
} catch (e) {
|
|
185
195
|
server = { ...server, [url]: { error: e instanceof Error ? e.message : String(e) } };
|
|
@@ -193,7 +203,10 @@
|
|
|
193
203
|
? (n / 1024).toFixed(1) + ' KB'
|
|
194
204
|
: (n / 1048576).toFixed(1) + ' MB';
|
|
195
205
|
|
|
196
|
-
onMount(() => () =>
|
|
206
|
+
onMount(() => () => {
|
|
207
|
+
clear_highlight();
|
|
208
|
+
owner.close();
|
|
209
|
+
});
|
|
197
210
|
</script>
|
|
198
211
|
|
|
199
212
|
<div class="rec" data-og-session>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scope } from "@ogygia/machine";
|
|
1
2
|
// src/devtools/highlight.ts
|
|
2
3
|
/**
|
|
3
4
|
* Light up MANY elements on the page at once — the profile's "these 290 shadow roots", "every
|
|
@@ -9,14 +10,16 @@ let root = null;
|
|
|
9
10
|
let layer = null;
|
|
10
11
|
let current = [];
|
|
11
12
|
let label = "";
|
|
12
|
-
|
|
13
|
+
/** The layer while it is shown: its scroll and resize watch and its queued frame. Hiding it closes. */
|
|
14
|
+
let shown = null;
|
|
15
|
+
let queued = false;
|
|
13
16
|
const MAX_BOXES = 400;
|
|
14
17
|
/** The dock's shadow root (ui-app.ts sets it at mount). */
|
|
15
18
|
function set_highlight_root(r) {
|
|
16
19
|
root = r;
|
|
17
20
|
}
|
|
18
21
|
function draw() {
|
|
19
|
-
|
|
22
|
+
queued = false;
|
|
20
23
|
if (!layer) return;
|
|
21
24
|
layer.textContent = "";
|
|
22
25
|
const vw = innerWidth;
|
|
@@ -40,7 +43,9 @@ function draw() {
|
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
const schedule = () => {
|
|
43
|
-
if (!
|
|
46
|
+
if (queued || !shown) return;
|
|
47
|
+
queued = true;
|
|
48
|
+
shown.timeout(draw, "frame");
|
|
44
49
|
};
|
|
45
50
|
/** Show boxes over `els` (with a caption); `scroll` brings the first into view. */
|
|
46
51
|
function highlight(els, caption = "", scroll = false) {
|
|
@@ -52,11 +57,12 @@ function highlight(els, caption = "", scroll = false) {
|
|
|
52
57
|
layer.setAttribute("data-og-highlight", "");
|
|
53
58
|
layer.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:2147483600";
|
|
54
59
|
root.prepend(layer);
|
|
55
|
-
|
|
60
|
+
shown = scope();
|
|
61
|
+
shown.on(globalThis, "scroll", schedule, {
|
|
56
62
|
passive: true,
|
|
57
63
|
capture: true
|
|
58
64
|
});
|
|
59
|
-
|
|
65
|
+
shown.on(globalThis, "resize", schedule, { passive: true });
|
|
60
66
|
}
|
|
61
67
|
if (scroll && els[0]) try {
|
|
62
68
|
els[0].scrollIntoView({
|
|
@@ -72,8 +78,9 @@ function clear_highlight() {
|
|
|
72
78
|
if (layer) {
|
|
73
79
|
layer.remove();
|
|
74
80
|
layer = null;
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
shown?.close();
|
|
82
|
+
shown = null;
|
|
83
|
+
queued = false;
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
/** How many boxes are shown now (tests). */
|
|
@@ -2,6 +2,7 @@ import { byte_ledger } from "./bytes.js";
|
|
|
2
2
|
import { all_regions, region_name } from "./regions.js";
|
|
3
3
|
import { dynamic_imports, static_imports } from "@ogygia/core/insights";
|
|
4
4
|
import { ISLAND_GRAPH_ATTR, decode_island_graph, tool_fetch, tool_made } from "@ogygia/core/internal/instruments";
|
|
5
|
+
import { STALE, flights } from "@ogygia/machine";
|
|
5
6
|
// src/devtools/ledger-dom.ts
|
|
6
7
|
/**
|
|
7
8
|
* The byte ledger's DOM side: the page's island graph (`script[data-ogygia-graph]`, a build writes
|
|
@@ -41,15 +42,11 @@ function runtime_parts(src) {
|
|
|
41
42
|
return parts.read;
|
|
42
43
|
}
|
|
43
44
|
async function read_parts(src) {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
cache.set(u, t);
|
|
50
|
-
}
|
|
51
|
-
return t;
|
|
52
|
-
};
|
|
45
|
+
const texts = flights({
|
|
46
|
+
name: "devtools runtime parts",
|
|
47
|
+
run: (u) => tool_fetch(u, { cache: "force-cache" }).then((r) => r.ok ? r.text() : "")
|
|
48
|
+
});
|
|
49
|
+
const text = (u) => texts.get(u).then((t) => t === STALE ? "" : t, () => "");
|
|
53
50
|
const self = import.meta.url;
|
|
54
51
|
const roots = /* @__PURE__ */ new Map();
|
|
55
52
|
const dock = [];
|
package/dist/devtools/names.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import { flights } from "@ogygia/machine";
|
|
1
2
|
import { names } from "virtual:ogygia/devtools-meta";
|
|
2
3
|
// src/devtools/names.ts
|
|
3
4
|
/**
|
|
4
5
|
* The islands' component names for the dock and the testing API: a build carries them in
|
|
5
6
|
* `virtual:ogygia/devtools-meta`; the dev server serves them live (`/__ogygia_devtools_meta`, the
|
|
6
7
|
* registry as it is now). Both land on `window.__ogygia_region_names`, which regions.ts reads.
|
|
7
|
-
* Once per page; best effort (a hash stands in for a name it cannot find).
|
|
8
|
+
* Once per page; best effort (a hash stands in for a name it cannot find). A fetch that failed (the
|
|
9
|
+
* network, not an answer) is not kept: the next ask fetches again.
|
|
8
10
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
loading = (async () => {
|
|
11
|
+
const loading = flights({
|
|
12
|
+
name: "devtools region names",
|
|
13
|
+
run: async () => {
|
|
13
14
|
if (typeof window === "undefined") return;
|
|
14
15
|
if (names && Object.keys(names).length) {
|
|
15
16
|
window.__ogygia_region_names = {
|
|
@@ -18,17 +19,17 @@ function ensure_region_names() {
|
|
|
18
19
|
};
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return loading;
|
|
22
|
+
const r = await fetch("/__ogygia_devtools_meta");
|
|
23
|
+
if (!r.ok) return;
|
|
24
|
+
const meta = await r.json().catch(() => null);
|
|
25
|
+
if (meta?.names) window.__ogygia_region_names = {
|
|
26
|
+
...meta.names,
|
|
27
|
+
...window.__ogygia_region_names
|
|
28
|
+
};
|
|
29
|
+
if (meta?.bytes) window.__ogygia_region_bytes = meta.bytes;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
function ensure_region_names() {
|
|
33
|
+
return loading.get().then(() => {}, () => {});
|
|
33
34
|
}
|
|
34
35
|
export { ensure_region_names };
|
package/dist/devtools/page.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface PageView {
|
|
|
34
34
|
holes_failed?: HoleFailure[];
|
|
35
35
|
}
|
|
36
36
|
export declare function region_facts(): RegionFact[];
|
|
37
|
+
export declare function prebundled_packages(script_urls: readonly string[]): string[] | null;
|
|
38
|
+
export declare function vary_of(url: string): string | null;
|
|
37
39
|
/** This page's hydration failures, each island once: after an in-app navigation, the ones since it,
|
|
38
40
|
* and an earlier one only while its region is still on the page and still not awake (a kept
|
|
39
41
|
* island). Every visit to a page with a broken island failed it again: without this, the tab listed
|
package/dist/devtools/page.js
CHANGED
|
@@ -6,6 +6,7 @@ import { with_source_lines } from "./source-lines.js";
|
|
|
6
6
|
import { since_load } from "./since-load.js";
|
|
7
7
|
import { analyze_cpu, analyze_page, browser_of, defer_keys, dev_compile_ms, fp_compare, is_trace, prebundled_names, vital_parts } from "@ogygia/core/insights";
|
|
8
8
|
import { PAGE_DEFER_KEY, PAGE_DEFER_REGISTRY_KEY, PAGE_SEED_SELECTOR, beacon_page, hole_request_times, is_headless, tool_fetch } from "@ogygia/core/internal/instruments";
|
|
9
|
+
import { flights } from "@ogygia/machine";
|
|
9
10
|
import { snapshot } from "@ogygia/core/events";
|
|
10
11
|
// src/devtools/page.ts
|
|
11
12
|
/**
|
|
@@ -166,29 +167,42 @@ function shown_under_media(el) {
|
|
|
166
167
|
/** THE DEV SERVER'S PRE-BUNDLED PACKAGES, read once from its `.vite/deps/_metadata.json` (the folder of
|
|
167
168
|
* a pre-bundled script the page loaded — no guess at the app's base): the names its `optimized` keys
|
|
168
169
|
* belong to. Null until read; never off the dev server, nor on a page that loaded no pre-bundled file. */
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
const prebundled = flights({
|
|
171
|
+
name: "devtools prebundled packages",
|
|
172
|
+
run: async (dir, signal) => {
|
|
173
|
+
const r = await tool_fetch(dir + "_metadata.json", { signal });
|
|
174
|
+
if (r.status >= 500) throw new Error(`status ${r.status}`);
|
|
175
|
+
const m = r.ok ? await r.json() : null;
|
|
176
|
+
return prebundled_names(Object.keys(m?.optimized ?? {}));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
171
179
|
function prebundled_packages(script_urls) {
|
|
172
|
-
if (!import.meta.env.DEV
|
|
180
|
+
if (!import.meta.env.DEV) return null;
|
|
173
181
|
const one = script_urls.find((u) => u.includes("/node_modules/.vite/deps/"));
|
|
174
182
|
if (!one) return null;
|
|
175
|
-
|
|
176
|
-
const dir = one.slice(0, one.indexOf("/node_modules/.vite/deps/") + 25);
|
|
177
|
-
tool_fetch(dir + "_metadata.json").then((r) => r.ok ? r.json() : null).then((m) => {
|
|
178
|
-
prebundled = prebundled_names(Object.keys(m?.optimized ?? {}));
|
|
179
|
-
}).catch(() => {});
|
|
180
|
-
return null;
|
|
183
|
+
return asked(prebundled, one.slice(0, one.indexOf("/node_modules/.vite/deps/") + 25));
|
|
181
184
|
}
|
|
182
185
|
/** A FILE THE BROWSER DOWNLOADED AGAIN: its answer's `Vary` header, asked once per URL with a HEAD
|
|
183
186
|
* (a tool's request, out of every measurement). Null until it is in, or when the answer has none. */
|
|
184
|
-
const varies =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
varies.set(url, null);
|
|
188
|
-
tool_fetch(url, {
|
|
187
|
+
const varies = flights({
|
|
188
|
+
name: "devtools vary",
|
|
189
|
+
run: (url, signal) => tool_fetch(url, {
|
|
189
190
|
method: "HEAD",
|
|
190
|
-
cache: "no-store"
|
|
191
|
-
|
|
191
|
+
cache: "no-store",
|
|
192
|
+
signal
|
|
193
|
+
}).then((r) => r.headers.get("vary"))
|
|
194
|
+
});
|
|
195
|
+
function vary_of(url) {
|
|
196
|
+
return asked(varies, url);
|
|
197
|
+
}
|
|
198
|
+
/** What the dev server answered for `key`, read without waiting: the kept answer, or null while it
|
|
199
|
+
* is asked (the first read starts the ask). An answer is kept for the session; a failed ask is not,
|
|
200
|
+
* so a later read asks again. A read while the document goes (the `pagehide` save) asks nothing: the
|
|
201
|
+
* request could not land, and WebKit says the cancelled fetch as a page error. */
|
|
202
|
+
function asked(f, key) {
|
|
203
|
+
const e = f.peek(key);
|
|
204
|
+
if (e?.kind === "kept") return e.answer;
|
|
205
|
+
if (e === void 0 && !leaving) f.get(key).catch(() => {});
|
|
192
206
|
return null;
|
|
193
207
|
}
|
|
194
208
|
function first_child_top(el) {
|
|
@@ -902,15 +916,21 @@ function install_page_hook() {
|
|
|
902
916
|
if (typeof window !== "undefined" && !hooked_hide) {
|
|
903
917
|
hooked_hide = true;
|
|
904
918
|
addEventListener("pagehide", () => {
|
|
919
|
+
leaving = true;
|
|
905
920
|
try {
|
|
906
921
|
const v = read_page();
|
|
907
922
|
if (v && !v.nav) sessionStorage.setItem(LOAD_KEY + location.pathname, JSON.stringify(snapshot_of(v)));
|
|
908
923
|
} catch {}
|
|
909
924
|
});
|
|
925
|
+
addEventListener("pageshow", () => {
|
|
926
|
+
leaving = false;
|
|
927
|
+
});
|
|
910
928
|
}
|
|
911
929
|
}
|
|
912
930
|
const LOAD_KEY = "ogygia:devtools:load:";
|
|
913
931
|
let hooked_hide = false;
|
|
932
|
+
/** The document is going (`pagehide`, until a `pageshow` brings it back): reads ask the server nothing. */
|
|
933
|
+
let leaving = false;
|
|
914
934
|
/** the previous load's picture of THIS page, read once per document (before this load overwrites it) */
|
|
915
935
|
let prev_load;
|
|
916
936
|
function previous_load() {
|
|
@@ -954,4 +974,4 @@ function snapshot_of(v) {
|
|
|
954
974
|
...v.report.findings.some((f) => f.dev_compile && (f.code === "shift-cause" || f.code === "vital-cls")) ? { cls_dev: true } : {}
|
|
955
975
|
};
|
|
956
976
|
}
|
|
957
|
-
export { cpu_of, failures, held_open, hole_batches, hole_failures, hole_waits, install_page_hook, last_nav, read_page, region_facts, since_nav, summary_lines, unmeasured };
|
|
977
|
+
export { cpu_of, failures, held_open, hole_batches, hole_failures, hole_waits, install_page_hook, last_nav, prebundled_packages, read_page, region_facts, since_nav, summary_lines, unmeasured, vary_of };
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* The devtools' copy of the last SSR profile per page — the native Profiler tab writes it, the Lens
|
|
3
|
-
* roster and the island detail read it (joined by fingerprint, then by entry), so a server cost sits
|
|
4
|
-
* next to the browser's numbers for the same island. Kept SLIM: the profiler's JSON answer is
|
|
5
|
-
* hundreds of KB; the dock needs the score, the forecast, the findings, the islands, the top
|
|
6
|
-
* components and the calls. The slim copy also survives a reload (sessionStorage, best effort).
|
|
7
|
-
*/
|
|
1
|
+
import { Scope } from "@ogygia/machine";
|
|
8
2
|
export interface SlimIsland {
|
|
9
3
|
name: string;
|
|
10
4
|
fp: string | null;
|
|
@@ -155,9 +149,10 @@ export interface SlimProfile {
|
|
|
155
149
|
}
|
|
156
150
|
/**
|
|
157
151
|
* Profile a same-origin path on the server, now (the profiler's `/page?format=json`; a 409 = the
|
|
158
|
-
* recorder is busy: wait and ask again). Stores the slim copy and returns it
|
|
152
|
+
* recorder is busy: wait and ask again, `fetch_when_free`). Stores the slim copy and returns it;
|
|
153
|
+
* `null` once `owner` (the tab that asked) ended first.
|
|
159
154
|
*/
|
|
160
|
-
export declare function run_profile(path: string, runs
|
|
155
|
+
export declare function run_profile(path: string, runs: number, base: string, owner: Scope): Promise<SlimProfile | null>;
|
|
161
156
|
/**
|
|
162
157
|
* A record of the profiler's JSON answer, untyped on the wire (and possibly from another version):
|
|
163
158
|
* read deep and tolerant (`r.since?.score?.a`), with every value checked before use (`num`, `str`,
|
|
@@ -1,30 +1,35 @@
|
|
|
1
|
+
import { fetch_when_free } from "../profiler/busy.js";
|
|
2
|
+
import { STALE } from "@ogygia/machine";
|
|
1
3
|
// src/devtools/profile-store.ts
|
|
2
4
|
/**
|
|
5
|
+
* The devtools' copy of the last SSR profile per page — the native Profiler tab writes it, the Lens
|
|
6
|
+
* roster and the island detail read it (joined by fingerprint, then by entry), so a server cost sits
|
|
7
|
+
* next to the browser's numbers for the same island. Kept SLIM: the profiler's JSON answer is
|
|
8
|
+
* hundreds of KB; the dock needs the score, the forecast, the findings, the islands, the top
|
|
9
|
+
* components and the calls. The slim copy also survives a reload (sessionStorage, best effort).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
3
12
|
* Profile a same-origin path on the server, now (the profiler's `/page?format=json`; a 409 = the
|
|
4
|
-
* recorder is busy: wait and ask again). Stores the slim copy and returns it
|
|
13
|
+
* recorder is busy: wait and ask again, `fetch_when_free`). Stores the slim copy and returns it;
|
|
14
|
+
* `null` once `owner` (the tab that asked) ended first.
|
|
5
15
|
*/
|
|
6
|
-
async function run_profile(path, runs
|
|
16
|
+
async function run_profile(path, runs, base, owner) {
|
|
7
17
|
const url = `${base}/page?p=${encodeURIComponent(path)}&runs=${runs}&format=json`;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
said = typeof j.error === "string" ? j.error : typeof j.message === "string" ? j.message : text;
|
|
20
|
-
} catch {}
|
|
21
|
-
throw new Error(r.status === 503 ? said.slice(0, 600) : `the profiler answered ${r.status}: ${said.slice(0, 300)}`);
|
|
22
|
-
}
|
|
23
|
-
const p = slim_profile(await r.json());
|
|
24
|
-
set_profile(p);
|
|
25
|
-
return p;
|
|
18
|
+
const r = await fetch_when_free(owner, url, { headers: { accept: "application/json" } });
|
|
19
|
+
if (r === STALE) return null;
|
|
20
|
+
if (r.status === 409) throw new Error("another profile kept the recorder busy for two minutes");
|
|
21
|
+
if (!r.ok) {
|
|
22
|
+
const text = await r.text();
|
|
23
|
+
let said = text;
|
|
24
|
+
try {
|
|
25
|
+
const j = JSON.parse(text);
|
|
26
|
+
said = typeof j.error === "string" ? j.error : typeof j.message === "string" ? j.message : text;
|
|
27
|
+
} catch {}
|
|
28
|
+
throw new Error(r.status === 503 ? said.slice(0, 600) : `the profiler answered ${r.status}: ${said.slice(0, 300)}`);
|
|
26
29
|
}
|
|
27
|
-
|
|
30
|
+
const p = slim_profile(await r.json());
|
|
31
|
+
set_profile(p);
|
|
32
|
+
return p;
|
|
28
33
|
}
|
|
29
34
|
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
30
35
|
const str = (v) => typeof v === "string" ? v : null;
|