@ogygia/devtools 0.12.0 → 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/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/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 +13 -4
package/dist/profiler/net.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { register_profiler_file } from "./frames.js";
|
|
2
|
+
import { Scope } from "@ogygia/machine";
|
|
2
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
4
|
export interface NetCall {
|
|
4
5
|
/** performance.now() at call start */
|
|
@@ -71,7 +72,10 @@ export interface ServerTiming {
|
|
|
71
72
|
desc?: string;
|
|
72
73
|
}
|
|
73
74
|
export declare function parse_server_timing(header: string | null | undefined): ServerTiming[] | undefined;
|
|
74
|
-
|
|
75
|
+
/** Turns `owner`'s ask for call stacks on or off. They are captured while any recording asks, so
|
|
76
|
+
* the first of two overlapping recordings to end (two profilers in one dev process) never turns
|
|
77
|
+
* them off under the other. */
|
|
78
|
+
export declare function set_stack_capture(owner: object, on: boolean): void;
|
|
75
79
|
/** A caller location as the bundler sees it — resolved to source later, at
|
|
76
80
|
* report time, through the sourcemap resolver (we have no maps here). */
|
|
77
81
|
export interface CallerSite {
|
|
@@ -100,12 +104,15 @@ export interface NetContext {
|
|
|
100
104
|
/** the profiler's what-if render: an answer kept from the timed renders, for a call this request
|
|
101
105
|
* makes (`GET <url>`), instead of making it — undefined = make the call */
|
|
102
106
|
answer?(method: string, url: string): Response | undefined;
|
|
107
|
+
/** the request's owner: what the capture reads for a call this request makes (the copy of its
|
|
108
|
+
* body it counts) is let go when the request ends */
|
|
109
|
+
owner?: Scope;
|
|
103
110
|
}
|
|
104
111
|
type Emit = (call: NetCall) => void;
|
|
105
|
-
/**
|
|
106
|
-
export declare function keep_answers(on: boolean): void;
|
|
107
|
-
/**
|
|
108
|
-
export declare function drop_answers(): void;
|
|
112
|
+
/** `owner` starts (the first recording to start clears what was kept) or stops keeping answers */
|
|
113
|
+
export declare function keep_answers(owner: object, on: boolean): void;
|
|
114
|
+
/** `owner` is done with the kept answers: dropped once no recording reads them */
|
|
115
|
+
export declare function drop_answers(owner: object): void;
|
|
109
116
|
/**
|
|
110
117
|
* WHAT CHANGED IN AN ANSWER between the first render and the last: the one stretch that differs
|
|
111
118
|
* (the bytes before it and after it are the same), and that stretch as text with a little around
|
|
@@ -121,7 +128,8 @@ export declare function kept_answer(key: string): Response | undefined;
|
|
|
121
128
|
/**
|
|
122
129
|
* Install the patches. `fallback` receives calls made outside any request
|
|
123
130
|
* context (startup work, background jobs) so window recordings still see them.
|
|
124
|
-
*
|
|
131
|
+
* A second call makes its own storage and fallback the live ones, and re-asserts the fetch patch
|
|
132
|
+
* (see `ensure_fetch_patched`).
|
|
125
133
|
*/
|
|
126
134
|
export declare function install_net_capture(als: AsyncLocalStorage<NetContext>, fallback: Emit): Promise<void>;
|
|
127
135
|
/**
|
package/dist/profiler/net.js
CHANGED
|
@@ -66,8 +66,14 @@ function parse_server_timing(header) {
|
|
|
66
66
|
}
|
|
67
67
|
const NET_STATE = Symbol.for("ogygia.profiler.net-state");
|
|
68
68
|
const shared = globalThis[NET_STATE] ??= { stacks: false };
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
/** Turns `owner`'s ask for call stacks on or off. They are captured while any recording asks, so
|
|
70
|
+
* the first of two overlapping recordings to end (two profilers in one dev process) never turns
|
|
71
|
+
* them off under the other. */
|
|
72
|
+
function set_stack_capture(owner, on) {
|
|
73
|
+
const owners = shared.owners ??= /* @__PURE__ */ new Set();
|
|
74
|
+
if (on) owners.add(owner);
|
|
75
|
+
else owners.delete(owner);
|
|
76
|
+
shared.stacks = owners.size > 0;
|
|
71
77
|
}
|
|
72
78
|
register_profiler_file();
|
|
73
79
|
/** The nearest first-party frame that issued an I/O call — skips node internals,
|
|
@@ -111,10 +117,10 @@ function nearest_app_frame() {
|
|
|
111
117
|
if (!s) return void 0;
|
|
112
118
|
return `${s.fn} (${base_name(s.file)}:${s.line})`;
|
|
113
119
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
/** A call goes to the live profiler (nowhere before one installed). */
|
|
121
|
+
function emit(call) {
|
|
122
|
+
shared.emit?.(call);
|
|
123
|
+
}
|
|
118
124
|
const KEEP_ONE = 1048576;
|
|
119
125
|
const KEEP_ALL = 8388608;
|
|
120
126
|
let keep_bodies = false;
|
|
@@ -122,21 +128,32 @@ let kept_bytes = 0;
|
|
|
122
128
|
const kept = /* @__PURE__ */ new Map();
|
|
123
129
|
/** each GET's FIRST answer in the recording too: against the last, what changed between renders */
|
|
124
130
|
const kept_first = /* @__PURE__ */ new Map();
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
kept_bytes = 0;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/** drop every kept answer */
|
|
135
|
-
function drop_answers() {
|
|
131
|
+
/** The recordings keeping answers now, and the ones still reading what was kept (a recording stops
|
|
132
|
+
* keeping after its timed renders, and reads them in its keep-the-answers renders after). Each
|
|
133
|
+
* recording turns only its own on and off: one that ends never stops or drops another's. */
|
|
134
|
+
const keepers = /* @__PURE__ */ new Set();
|
|
135
|
+
const readers = /* @__PURE__ */ new Set();
|
|
136
|
+
function clear_kept() {
|
|
136
137
|
kept.clear();
|
|
137
138
|
kept_first.clear();
|
|
138
139
|
kept_bytes = 0;
|
|
139
140
|
}
|
|
141
|
+
/** `owner` starts (the first recording to start clears what was kept) or stops keeping answers */
|
|
142
|
+
function keep_answers(owner, on) {
|
|
143
|
+
if (on) {
|
|
144
|
+
if (readers.size === 0) clear_kept();
|
|
145
|
+
keepers.add(owner);
|
|
146
|
+
readers.add(owner);
|
|
147
|
+
} else keepers.delete(owner);
|
|
148
|
+
keep_bodies = keepers.size > 0;
|
|
149
|
+
}
|
|
150
|
+
/** `owner` is done with the kept answers: dropped once no recording reads them */
|
|
151
|
+
function drop_answers(owner) {
|
|
152
|
+
keepers.delete(owner);
|
|
153
|
+
readers.delete(owner);
|
|
154
|
+
keep_bodies = keepers.size > 0;
|
|
155
|
+
if (readers.size === 0) clear_kept();
|
|
156
|
+
}
|
|
140
157
|
/**
|
|
141
158
|
* WHAT CHANGED IN AN ANSWER between the first render and the last: the one stretch that differs
|
|
142
159
|
* (the bytes before it and after it are the same), and that stretch as text with a little around
|
|
@@ -176,11 +193,13 @@ const OG_FETCH_PATCH = Symbol.for("ogygia.profiler.net.fetch-patch");
|
|
|
176
193
|
/**
|
|
177
194
|
* Install the patches. `fallback` receives calls made outside any request
|
|
178
195
|
* context (startup work, background jobs) so window recordings still see them.
|
|
179
|
-
*
|
|
196
|
+
* A second call makes its own storage and fallback the live ones, and re-asserts the fetch patch
|
|
197
|
+
* (see `ensure_fetch_patched`).
|
|
180
198
|
*/
|
|
181
199
|
async function install_net_capture(als, fallback) {
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
shared.answer = (method, url) => als.getStore()?.answer?.(method, url);
|
|
201
|
+
shared.owner = () => als.getStore()?.owner;
|
|
202
|
+
shared.emit = (call) => {
|
|
184
203
|
const ctx = als.getStore();
|
|
185
204
|
if (ctx) {
|
|
186
205
|
call.route = ctx.route;
|
|
@@ -188,13 +207,10 @@ async function install_net_capture(als, fallback) {
|
|
|
188
207
|
ctx.on_net(call);
|
|
189
208
|
} else fallback(call);
|
|
190
209
|
};
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
installed = true;
|
|
196
|
-
patch_fetch(net_emit);
|
|
197
|
-
await patch_http(net_emit);
|
|
210
|
+
patch_fetch();
|
|
211
|
+
if (shared.http_patched) return;
|
|
212
|
+
shared.http_patched = true;
|
|
213
|
+
await patch_http();
|
|
198
214
|
}
|
|
199
215
|
/**
|
|
200
216
|
* Re-assert the `globalThis.fetch` patch. `globalThis.fetch` can be REPLACED out from under us after
|
|
@@ -205,10 +221,7 @@ async function install_net_capture(als, fallback) {
|
|
|
205
221
|
* call every request/run.
|
|
206
222
|
*/
|
|
207
223
|
function ensure_fetch_patched() {
|
|
208
|
-
if (
|
|
209
|
-
const cur = globalThis.fetch;
|
|
210
|
-
if (typeof cur === "function" && cur[OG_FETCH_PATCH]) return;
|
|
211
|
-
patch_fetch(net_emit);
|
|
224
|
+
if (shared.emit) patch_fetch();
|
|
212
225
|
}
|
|
213
226
|
const INTERNAL_HEADER = "x-og-profiler-internal";
|
|
214
227
|
function is_internal(input, init) {
|
|
@@ -226,11 +239,11 @@ function host_of(url) {
|
|
|
226
239
|
return "";
|
|
227
240
|
}
|
|
228
241
|
}
|
|
229
|
-
function patch_fetch(
|
|
242
|
+
function patch_fetch() {
|
|
230
243
|
const orig = globalThis.fetch;
|
|
231
244
|
if (typeof orig !== "function") return;
|
|
232
245
|
if (orig[OG_FETCH_PATCH]) return;
|
|
233
|
-
globalThis.fetch = wrap_fetch(orig
|
|
246
|
+
globalThis.fetch = wrap_fetch(orig);
|
|
234
247
|
}
|
|
235
248
|
/**
|
|
236
249
|
* KIT'S OWN `event.fetch` never reaches the global one for a same-origin call: it dispatches the
|
|
@@ -239,8 +252,8 @@ function patch_fetch(emit) {
|
|
|
239
252
|
* the same recorder. Idempotent (the brand), nothing when capture is not installed.
|
|
240
253
|
*/
|
|
241
254
|
function wrap_event_fetch(f) {
|
|
242
|
-
if (!
|
|
243
|
-
return wrap_fetch(f
|
|
255
|
+
if (!shared.emit || typeof f !== "function" || f[OG_FETCH_PATCH]) return f;
|
|
256
|
+
return wrap_fetch(f);
|
|
244
257
|
}
|
|
245
258
|
/** the request header that asks, and the response header that answers */
|
|
246
259
|
const TRACE_HEADER = "x-og-trace";
|
|
@@ -281,13 +294,13 @@ function decode_trace(h) {
|
|
|
281
294
|
}
|
|
282
295
|
}
|
|
283
296
|
/** The recording wrapper around a fetch-shaped function: one NetCall per call, timed to headers
|
|
284
|
-
* and body, the caller captured while a recording runs. */
|
|
285
|
-
function wrap_fetch(orig
|
|
297
|
+
* and body, the caller captured while a recording runs, handed to the live profiler. */
|
|
298
|
+
function wrap_fetch(orig) {
|
|
286
299
|
const patched = async function fetch(input, init) {
|
|
287
300
|
if (is_internal(input, init)) return orig(input, init);
|
|
288
301
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input?.url ?? "";
|
|
289
302
|
const method = init?.method ?? (input?.method || "GET");
|
|
290
|
-
const canned =
|
|
303
|
+
const canned = shared.answer?.(method.toUpperCase(), url);
|
|
291
304
|
if (canned) return canned;
|
|
292
305
|
const call = {
|
|
293
306
|
start: performance.now(),
|
|
@@ -335,7 +348,7 @@ function wrap_fetch(orig, emit) {
|
|
|
335
348
|
call.timings = parse_server_timing(res.headers.get("server-timing"));
|
|
336
349
|
const trace = decode_trace(res.headers.get(TRACE_HEADER));
|
|
337
350
|
if (trace) call.trace = trace;
|
|
338
|
-
count_decoded(res, call);
|
|
351
|
+
count_decoded(res, call, shared.owner?.());
|
|
339
352
|
wrap_body(res, call);
|
|
340
353
|
return res;
|
|
341
354
|
} catch (e) {
|
|
@@ -419,8 +432,10 @@ function capture_req_payload(init, call) {
|
|
|
419
432
|
/** a body is fingerprinted over this many bytes at most (with its full length) */
|
|
420
433
|
const BODY_HASH_MAX = 4194304;
|
|
421
434
|
/** Count the DECODED response body size by draining a CLONE in the background (the original is left
|
|
422
|
-
* untouched). Sets `call.bytes` when done — the render finishes first, so it's ready by report time.
|
|
423
|
-
|
|
435
|
+
* untouched). Sets `call.bytes` when done — the render finishes first, so it's ready by report time.
|
|
436
|
+
* The clone is the request's (`owner`): when the request ends it is let go, so a body that never
|
|
437
|
+
* ends is not read for good. */
|
|
438
|
+
function count_decoded(res, call, owner) {
|
|
424
439
|
let clone;
|
|
425
440
|
try {
|
|
426
441
|
clone = res.clone();
|
|
@@ -434,6 +449,7 @@ function count_decoded(res, call) {
|
|
|
434
449
|
(async () => {
|
|
435
450
|
try {
|
|
436
451
|
const reader = stream.getReader();
|
|
452
|
+
owner?.defer(() => void reader.cancel().catch(() => {}));
|
|
437
453
|
let total = 0;
|
|
438
454
|
let parts = keep ? [] : null;
|
|
439
455
|
let h = 2166136261;
|
|
@@ -481,7 +497,7 @@ function count_decoded(res, call) {
|
|
|
481
497
|
} catch {}
|
|
482
498
|
})();
|
|
483
499
|
}
|
|
484
|
-
async function patch_http(
|
|
500
|
+
async function patch_http() {
|
|
485
501
|
for (const proto of ["http", "https"]) {
|
|
486
502
|
let mod;
|
|
487
503
|
try {
|
|
@@ -498,18 +514,18 @@ async function patch_http(emit) {
|
|
|
498
514
|
try {
|
|
499
515
|
mod.request = function(...args) {
|
|
500
516
|
const req = orig_request.apply(this, args);
|
|
501
|
-
instrument_client_request(req, args, proto
|
|
517
|
+
instrument_client_request(req, args, proto);
|
|
502
518
|
return req;
|
|
503
519
|
};
|
|
504
520
|
if (typeof orig_get === "function") mod.get = function(...args) {
|
|
505
521
|
const req = orig_get.apply(this, args);
|
|
506
|
-
instrument_client_request(req, args, proto
|
|
522
|
+
instrument_client_request(req, args, proto);
|
|
507
523
|
return req;
|
|
508
524
|
};
|
|
509
525
|
} catch {}
|
|
510
526
|
}
|
|
511
527
|
}
|
|
512
|
-
function instrument_client_request(req, args, proto
|
|
528
|
+
function instrument_client_request(req, args, proto) {
|
|
513
529
|
const r = req;
|
|
514
530
|
if (typeof r?.on !== "function") return;
|
|
515
531
|
let url = "";
|
|
@@ -1334,6 +1334,33 @@ const CHAIN_GAP_MS = 10;
|
|
|
1334
1334
|
const CHAIN_MIN = 3;
|
|
1335
1335
|
const CHAIN_MIN_MS = 20;
|
|
1336
1336
|
/**
|
|
1337
|
+
* The calls that started together (under 1 ms apart) with another call from the same calling line:
|
|
1338
|
+
* one of a batch run in parallel (a `Promise.all`). Each calling line's calls are sorted by start
|
|
1339
|
+
* once, so the call that started nearest to one is beside it.
|
|
1340
|
+
*/
|
|
1341
|
+
function batched_calls(calls) {
|
|
1342
|
+
const by_line = /* @__PURE__ */ new Map();
|
|
1343
|
+
for (const c of calls) {
|
|
1344
|
+
const line = c.outer?.line ?? c.at?.line;
|
|
1345
|
+
if (Number.isNaN(line) || !Number.isFinite(c.start)) continue;
|
|
1346
|
+
const path = c.outer?.path ?? c.at?.path;
|
|
1347
|
+
let lines = by_line.get(path);
|
|
1348
|
+
if (!lines) by_line.set(path, lines = /* @__PURE__ */ new Map());
|
|
1349
|
+
let same = lines.get(line);
|
|
1350
|
+
if (!same) lines.set(line, same = /* @__PURE__ */ new Set());
|
|
1351
|
+
same.add(c);
|
|
1352
|
+
}
|
|
1353
|
+
const out = /* @__PURE__ */ new Set();
|
|
1354
|
+
for (const lines of by_line.values()) for (const same of lines.values()) {
|
|
1355
|
+
const sorted = [...same].sort((a, b) => a.start - b.start);
|
|
1356
|
+
for (let i = 1; i < sorted.length; i++) if (Math.abs(sorted[i].start - sorted[i - 1].start) < 1) {
|
|
1357
|
+
out.add(sorted[i]);
|
|
1358
|
+
out.add(sorted[i - 1]);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return out;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1337
1364
|
* THE WAITS IN A ROW: calls made from ONE line whose waits did not overlap — each started after
|
|
1338
1365
|
* the one before it ended — so the line waited for them one by one (`for (…) await fetch(…)`).
|
|
1339
1366
|
* Measured, not guessed: the sum of the waits is what the line cost in wall time, the longest
|
|
@@ -1367,7 +1394,8 @@ function find_wait_patterns(input) {
|
|
|
1367
1394
|
if (g.length < 2) continue;
|
|
1368
1395
|
g.sort((a, b) => a.start - b.start);
|
|
1369
1396
|
const runs = [];
|
|
1370
|
-
const
|
|
1397
|
+
const in_batch = batched_calls(g);
|
|
1398
|
+
const batched = (c) => in_batch.has(c);
|
|
1371
1399
|
let run = batched(g[0]) ? [] : [g[0]];
|
|
1372
1400
|
for (let i = 1; i < g.length; i++) {
|
|
1373
1401
|
if (batched(g[i])) {
|
|
@@ -1049,6 +1049,7 @@ function constant_work(source, spans = false) {
|
|
|
1049
1049
|
});
|
|
1050
1050
|
}
|
|
1051
1051
|
}
|
|
1052
|
+
const helper_names = new Set(helpers.map((h) => h.name));
|
|
1052
1053
|
const impure = (t) => {
|
|
1053
1054
|
for (let k = 0; k < t.length; k++) {
|
|
1054
1055
|
const x = t[k];
|
|
@@ -1056,21 +1057,31 @@ function constant_work(source, spans = false) {
|
|
|
1056
1057
|
if (per_call.has(x) || PER_CALL_GLOBALS.has(x)) return true;
|
|
1057
1058
|
if (x === "Math" && t[k + 2] === "random") return true;
|
|
1058
1059
|
if (x === "Date" && (t[k + 2] === "now" || t[k - 1] === "new" && t[k + 1] === "(" && t[k + 2] === ")")) return true;
|
|
1059
|
-
if (
|
|
1060
|
+
if (helper_names.has(x) && !module_names.has(x)) return true;
|
|
1060
1061
|
}
|
|
1061
1062
|
return changed_names(t).some((n) => state.has(n));
|
|
1062
1063
|
};
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
break;
|
|
1071
|
-
}
|
|
1064
|
+
const named_by = /* @__PURE__ */ new Map();
|
|
1065
|
+
for (const h of helpers) for (let i = h.from; i <= h.to; i++) {
|
|
1066
|
+
const t = T[i];
|
|
1067
|
+
for (let k = 0; k < t.length; k++) if (helper_names.has(t[k]) && t[k - 1] !== "." && t[k - 1] !== "?.") {
|
|
1068
|
+
const by = named_by.get(t[k]);
|
|
1069
|
+
if (by) by.push(h.name);
|
|
1070
|
+
else named_by.set(t[k], [h.name]);
|
|
1072
1071
|
}
|
|
1073
1072
|
}
|
|
1073
|
+
const dropped = [];
|
|
1074
|
+
for (const h of helpers) {
|
|
1075
|
+
if (!module_names.has(h.name)) continue;
|
|
1076
|
+
for (let i = h.from; i <= h.to; i++) if (impure(T[i])) {
|
|
1077
|
+
dropped.push(h.name);
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
while (dropped.length) {
|
|
1082
|
+
const name = dropped.pop();
|
|
1083
|
+
if (module_names.delete(name)) for (const by of named_by.get(name) ?? []) dropped.push(by);
|
|
1084
|
+
}
|
|
1074
1085
|
const changed = /* @__PURE__ */ new Set();
|
|
1075
1086
|
for (let i = body0 + 1; i < body1; i++) for (const n of changed_names(T[i])) changed.add(n);
|
|
1076
1087
|
const local = /* @__PURE__ */ new Set();
|
|
@@ -1163,8 +1174,9 @@ function constant_work(source, spans = false) {
|
|
|
1163
1174
|
});
|
|
1164
1175
|
}
|
|
1165
1176
|
}
|
|
1166
|
-
const
|
|
1167
|
-
for (
|
|
1177
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
1178
|
+
for (let i = body0 + 1; i < body1; i++) if (!stmt_lines.has(i)) for (const x of T[i]) elsewhere.add(x);
|
|
1179
|
+
for (const o of out) o.calls = o.calls.filter((c) => !elsewhere.has(c));
|
|
1168
1180
|
return out;
|
|
1169
1181
|
}
|
|
1170
1182
|
/**
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* // src/hooks.server.ts
|
|
15
15
|
* import { setProfilerStore } from '@ogygia/devtools/profiler/storage';
|
|
16
|
-
* import {
|
|
17
|
-
* setProfilerStore(
|
|
16
|
+
* import { SqliteStore } from '@ogygia/devtools/profiler/storage/sqlite';
|
|
17
|
+
* setProfilerStore(new SqliteStore('.ogygia/profiles.db'));
|
|
18
18
|
*
|
|
19
19
|
* A report is stored as its portable dump (the same JSON the `.ogp` export and the agent view are
|
|
20
20
|
* built from): `meta` + `analysis` + `extras`, plus a few summary columns for the list. Nothing
|
|
@@ -79,7 +79,18 @@ export interface ProfilerStore {
|
|
|
79
79
|
/** An in-memory store — a Map with an LRU cap. The profiler without a store keeps reports in its own
|
|
80
80
|
* memory already, so this adds nothing there; it is for tests and a model for a custom store.
|
|
81
81
|
* Nothing survives a restart. */
|
|
82
|
-
export declare
|
|
82
|
+
export declare class MemoryStore implements ProfilerStore {
|
|
83
|
+
#private;
|
|
84
|
+
readonly kind: string;
|
|
85
|
+
constructor(cap?: number);
|
|
86
|
+
putReport(rec: ProfilerRecord): void;
|
|
87
|
+
getReport(id: string): string | null;
|
|
88
|
+
listReports(limit?: number): ProfilerSummary[];
|
|
89
|
+
deleteReport(id: string): void;
|
|
90
|
+
prune(keep: number): void;
|
|
91
|
+
putVisit(rec: ProfilerVisitRecord): void;
|
|
92
|
+
listVisits(page: string, limit?: number): ProfilerVisitRecord[];
|
|
93
|
+
}
|
|
83
94
|
/** Pull the summary columns off a record for storing them alongside the dump. */
|
|
84
95
|
export declare function recordColumns(rec: ProfilerRecord): ProfilerSummary;
|
|
85
96
|
export declare function setProfilerStore(store: ProfilerStore): void;
|
|
@@ -2,45 +2,45 @@
|
|
|
2
2
|
/** An in-memory store — a Map with an LRU cap. The profiler without a store keeps reports in its own
|
|
3
3
|
* memory already, so this adds nothing there; it is for tests and a model for a custom store.
|
|
4
4
|
* Nothing survives a restart. */
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
putVisit(rec) {
|
|
36
|
-
visits.push(rec);
|
|
37
|
-
if (visits.length > 2e3) visits.shift();
|
|
38
|
-
},
|
|
39
|
-
listVisits(page, limit = 10) {
|
|
40
|
-
return visits.filter((v) => v.page === page).sort((a, b) => b.at - a.at).slice(0, limit);
|
|
5
|
+
var MemoryStore = class {
|
|
6
|
+
kind = "memory";
|
|
7
|
+
#cap;
|
|
8
|
+
#reports = /* @__PURE__ */ new Map();
|
|
9
|
+
#visits = [];
|
|
10
|
+
constructor(cap = 50) {
|
|
11
|
+
this.#cap = cap;
|
|
12
|
+
}
|
|
13
|
+
putReport(rec) {
|
|
14
|
+
this.#reports.delete(rec.id);
|
|
15
|
+
this.#reports.set(rec.id, rec);
|
|
16
|
+
this.prune(this.#cap);
|
|
17
|
+
}
|
|
18
|
+
getReport(id) {
|
|
19
|
+
return this.#reports.get(id)?.dump ?? null;
|
|
20
|
+
}
|
|
21
|
+
listReports(limit = 30) {
|
|
22
|
+
return [...this.#reports.values()].sort((a, b) => b.created - a.created).slice(0, limit).map(({ dump: _dump, ...s }) => s);
|
|
23
|
+
}
|
|
24
|
+
deleteReport(id) {
|
|
25
|
+
this.#reports.delete(id);
|
|
26
|
+
}
|
|
27
|
+
prune(keep) {
|
|
28
|
+
const reports = this.#reports;
|
|
29
|
+
while (reports.size > keep) {
|
|
30
|
+
const oldest = reports.keys().next().value;
|
|
31
|
+
if (oldest === void 0) break;
|
|
32
|
+
reports.delete(oldest);
|
|
41
33
|
}
|
|
42
|
-
}
|
|
43
|
-
|
|
34
|
+
}
|
|
35
|
+
putVisit(rec) {
|
|
36
|
+
const visits = this.#visits;
|
|
37
|
+
visits.push(rec);
|
|
38
|
+
if (visits.length > 2e3) visits.shift();
|
|
39
|
+
}
|
|
40
|
+
listVisits(page, limit = 10) {
|
|
41
|
+
return this.#visits.filter((v) => v.page === page).sort((a, b) => b.at - a.at).slice(0, limit);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
44
|
/** Pull the summary columns off a record for storing them alongside the dump. */
|
|
45
45
|
function recordColumns(rec) {
|
|
46
46
|
return {
|
|
@@ -59,8 +59,8 @@ function recordColumns(rec) {
|
|
|
59
59
|
*
|
|
60
60
|
* // src/hooks.server.ts
|
|
61
61
|
* import { setProfilerStore } from '@ogygia/devtools/profiler/storage';
|
|
62
|
-
* import {
|
|
63
|
-
* setProfilerStore(
|
|
62
|
+
* import { PostgresStore } from '@ogygia/devtools/profiler/storage/postgres';
|
|
63
|
+
* setProfilerStore(new PostgresStore(process.env.DATABASE_URL!));
|
|
64
64
|
*
|
|
65
65
|
* The profiler reads this when it mounts and every finished report is written there. Call it before
|
|
66
66
|
* the profiler handles its first request; a later call is ignored (the instance already mounted).
|
|
@@ -73,4 +73,4 @@ function setProfilerStore(store) {
|
|
|
73
73
|
function getProfilerStore() {
|
|
74
74
|
return globalThis[STORE_SLOT];
|
|
75
75
|
}
|
|
76
|
-
export {
|
|
76
|
+
export { MemoryStore, getProfilerStore, recordColumns, setProfilerStore };
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
import { ProfilerStore } from "./index.js";
|
|
1
|
+
import { ProfilerRecord, ProfilerStore, ProfilerSummary, ProfilerVisitRecord } from "./index.js";
|
|
2
2
|
interface PgLike {
|
|
3
3
|
query(text: string, params?: unknown[]): Promise<{
|
|
4
4
|
rows: Record<string, unknown>[];
|
|
5
5
|
}>;
|
|
6
6
|
end?(): Promise<void>;
|
|
7
7
|
}
|
|
8
|
-
export declare
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
export declare class PostgresStore implements ProfilerStore {
|
|
9
|
+
#private;
|
|
10
|
+
readonly kind: string;
|
|
11
|
+
constructor(client: PgLike | string, opts?: {
|
|
12
|
+
schema?: string;
|
|
13
|
+
table?: string;
|
|
14
|
+
});
|
|
15
|
+
init(): Promise<void>;
|
|
16
|
+
putReport(rec: ProfilerRecord): Promise<void>;
|
|
17
|
+
getReport(id: string): Promise<string | null>;
|
|
18
|
+
listReports(limit?: number): Promise<ProfilerSummary[]>;
|
|
19
|
+
deleteReport(id: string): Promise<void>;
|
|
20
|
+
prune(keep: number): Promise<void>;
|
|
21
|
+
putVisit(rec: ProfilerVisitRecord): Promise<void>;
|
|
22
|
+
listVisits(page: string, limit?: number): Promise<ProfilerVisitRecord[]>;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
}
|
|
12
25
|
export {};
|