zeitzeuge 0.6.0 → 0.6.2

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/cli.js CHANGED
@@ -1,742 +1,108 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- var __defProp = Object.defineProperty;
4
- var __export = (target, all) => {
5
- for (var name in all)
6
- __defProp(target, name, {
7
- get: all[name],
8
- enumerable: true,
9
- configurable: true,
10
- set: (newValue) => all[name] = () => newValue
11
- });
12
- };
13
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
15
4
 
16
- // src/analysis/prompts.ts
17
- var SYSTEM_PROMPT = `You are an expert web performance engineer. You have access to a virtual filesystem workspace containing captured data from a real page load: heap snapshot, network trace, and Chrome runtime trace.
18
-
19
- ## Workspace structure
20
-
21
- - /heap/summary.json — Parsed V8 heap snapshot: largest objects, type stats, constructor stats, detached DOM nodes, closure stats
22
- - /trace/summary.json — Page load metrics: timing, long tasks, render-blocking resources, resource breakdown
23
- - /trace/network-waterfall.json — Every network request with timing, size, priority, render-blocking status
24
- - /trace/asset-manifest.json — Index of all assets with paths to stored files
25
- - /trace/runtime/summary.json — Runtime trace overview: frame breakdown (scripting/layout/paint/GC), blocking function count, listener imbalances, GC stats
26
- - /trace/runtime/blocking-functions.json — Functions that blocked the main thread > 50ms, with script URL, line number, call stack, and duration
27
- - /trace/runtime/event-listeners.json — Event listener add/remove counts per event type, with source locations
28
- - /trace/runtime/frame-breakdown.json — Time spent in scripting vs layout vs paint vs GC
29
- - /trace/runtime/raw-events.json — Full Chrome trace events (large file — read to investigate specific function calls, layouts, GC, and event dispatches)
30
- - /scripts/*.js — Actual JavaScript source files captured during page load
31
- - /styles/*.css — Actual CSS source files
32
- - /html/document.html — The HTML document
33
-
34
- ## Your workflow
35
-
36
- 1. Read /heap/summary.json, /trace/summary.json, AND /trace/runtime/summary.json first for the big picture
37
- 2. Identify the highest-impact issues from all datasets
38
- 3. For each issue, dive into the relevant source files to understand the root cause
39
- 4. Provide specific, code-level fixes
40
-
41
- ## What to look for
42
-
43
- ### Memory issues (from heap data)
44
- - Memory leaks: unbounded arrays, maps, caches that grow without bound
45
- - Detached DOM nodes: DOM elements removed from the document but still referenced
46
- - Large retained objects: single objects or trees retaining disproportionate memory
47
- - Closure leaks: closures capturing variables they no longer need
48
-
49
- ### Page-load issues (from trace + source code)
50
- - Render-blocking scripts: <script> in <head> without async/defer — read the script to judge if it must be synchronous
51
- - Render-blocking CSS: large stylesheets blocking first paint
52
- - Long tasks (> 50ms): identify the function/module causing the block by reading the source
53
- - Large bundles: scripts > 100KB — search for unused imports or code that could be lazy-loaded
54
- - Sequential waterfalls: resources chained sequentially that could load in parallel
55
-
56
- ### Runtime issues (from Chrome trace)
57
- - Frame-blocking functions: read /trace/runtime/blocking-functions.json first, then inspect the actual script source at the reported line number to understand what the function does and how to optimize it
58
- - Event listener leaks: check /trace/runtime/event-listeners.json for event types where addCount >> removeCount, then grep the scripts for those addEventListener calls
59
- - GC pressure: high GC pause counts or duration suggest excessive short-lived object creation — look for hot loops creating objects
60
- - Layout thrashing: forced synchronous layouts caused by reading layout properties (offsetHeight, getBoundingClientRect) after DOM writes
61
-
62
- ## Severity classification
63
-
64
- Assign severity based on measured impact — do NOT guess:
65
-
66
- - **critical** — A blocking function >500ms, retained heap object >5MB,
67
- render-blocking resource >200KB, listener addCount > 10× removeCount,
68
- or GC pauses totalling >500ms
69
- - **warning** — A blocking function 100–500ms, retained heap object 1–5MB,
70
- render-blocking resource 50–200KB, listener addCount > 2× removeCount,
71
- or GC pauses totalling 100–500ms
72
- - **info** — Blocking function 50–100ms, retained object <1MB, minor
73
- optimisation opportunities, or observations about dependency usage
74
-
75
- Always base severity on the actual numbers from the captured data — never inflate.
76
-
77
- ## Verification rules
78
-
79
- These rules are mandatory for every finding:
80
-
81
- 1. **ALWAYS read the source file** in /scripts/, /styles/, or /html/ and
82
- verify the code BEFORE suggesting a fix. Never suggest a fix for code
83
- you have not read.
84
- 2. **Never guess at line numbers** — confirm by reading the file. If the
85
- trace reports a line number but the source doesn't match, say so.
86
- 3. Each \`suggestedFix\` MUST reference the actual current code and describe
87
- what to change. Include a before/after snippet from the real source.
88
- 4. If the heap summary, trace summary, and runtime summary all show healthy
89
- numbers, state that the page is well-optimised — do NOT manufacture
90
- findings.
91
- 5. Never report a finding based solely on a URL or resource name — always
92
- read the actual content to confirm the issue.
93
-
94
- ## Cross-referencing data
95
-
96
- - When a script appears in BOTH /trace/runtime/blocking-functions.json (CPU)
97
- AND /heap/summary.json (memory), mention both dimensions in the finding.
98
- - Check /trace/runtime/event-listeners.json for listener imbalances and
99
- cross-reference with the actual addEventListener calls in /scripts/.
100
- - Use /trace/network-waterfall.json to identify sequential chains, then read
101
- the initiating script to confirm the dependency.
102
- - When GC pauses are significant, cross-reference with heap data to identify
103
- which constructors or allocation patterns are responsible.
104
-
105
- ## Estimating impactMs
106
-
107
- Every finding should include an \`impactMs\` estimate when possible:
108
-
109
- - For render-blocking resources: impactMs ≈ the resource's load duration
110
- (from the network waterfall) that could be deferred.
111
- - For blocking functions: impactMs ≈ the function's duration minus 50ms
112
- (the long-task threshold).
113
- - For memory issues: impactMs may not apply — use \`retainedSize\` instead.
114
- - If you cannot reasonably estimate the savings, omit impactMs rather than
115
- guessing.
116
-
117
- ## Output guidelines
118
-
119
- - Report 3–7 findings, ordered by impact (mix of memory, page-load, and runtime if all have issues)
120
- - Be specific — name actual files, functions, object constructors, and retention paths
121
- - Provide concrete code fixes, not generic advice
122
- - If heap, trace, and runtime all look healthy, say so — don't manufacture issues
123
-
124
- ## Structured output fields
125
-
126
- For each finding, fill in as many fields as applicable:
127
-
128
- - \`sourceFile\` — the workspace path (e.g. /scripts/app.js) or resource URL
129
- where the issue occurs. Always set this when you can identify the file.
130
- - \`lineNumber\` — the 1-based line number in the source file. Only set after
131
- verifying by reading the file.
132
- - \`confidence\` — \`high\` if you read the source and confirmed the issue,
133
- \`medium\` if the profiling data strongly suggests it but you couldn't fully
134
- verify, \`low\` if inferred from patterns.
135
- - \`estimatedSavingsMs\` — your estimate of time saved if the fix is applied.
136
- - \`beforeCode\` — a snippet of the CURRENT problematic code, copied from the
137
- source file you read. Keep it focused (5–15 lines).
138
- - \`afterCode\` — the IMPROVED code snippet showing the fix. Must be a drop-in
139
- replacement for \`beforeCode\`.
140
- - \`impactMs\` — the current measured cost (e.g. blocking function duration,
141
- resource load time).`;
142
-
143
- // src/vitest/prompts.ts
144
- var VITEST_SYSTEM_PROMPT = `You are an expert in JavaScript/TypeScript performance optimization.
145
- You have access to a workspace containing V8 CPU profiling data captured during
146
- a Vitest test run. The workspace may also include V8 heap profiling data
147
- captured via Node's \`--heap-prof\` (allocation sampling).
148
-
149
- The profiling data covers BOTH the test code AND the application code being tested.
150
-
151
- **Your primary goal is to analyze the PERFORMANCE OF THE APPLICATION CODE
152
- being tested** — the functions, modules, and algorithms that the developer
153
- wrote and is benchmarking or testing. Test infrastructure overhead (Vitest,
154
- tinybench, test setup) is secondary context.
155
-
156
- ## Source categories
157
-
158
- Every hot function and script in the workspace has a \`sourceCategory\` field:
159
-
160
- - **application** — Code in the user's project (the code being tested).
161
- This is your PRIMARY focus. Find bottlenecks, inefficiencies, and
162
- optimization opportunities in these functions.
163
- - **dependency** — Third-party code in node_modules. Report when a dependency
164
- is a significant bottleneck, since the developer may be able to choose
165
- an alternative, configure it differently, or avoid calling it in hot paths.
166
- - **test** — Test files. Only mention if the test setup itself is creating
167
- artificial overhead that masks application performance.
168
- - **framework** — Vitest/tinybench/V8 internals. Generally ignore unless
169
- they dominate the profile in an unexpected way.
170
-
171
- ## Workspace structure
172
-
173
- - /summary.json — Overall test run: total tests, duration, pass/fail, GC stats
174
- - /timing/overview.json — Per-file test durations and individual test times
175
- - /timing/slow-tests.json — Tests exceeding the slow threshold
176
- - /profiles/index.json — Manifest mapping test files to their CPU profiles
177
- - /profiles/<file>.json — CPU profile summary: hot functions (with sourceCategory),
178
- call trees, GC samples, script breakdown (with sourceCategory)
179
- - /heap-profiles/index.json — (optional) Manifest mapping test files to heap profiles
180
- - /heap-profiles/<file>.json — (optional) Heap profile summary: allocation hotspots,
181
- per-script allocated bytes (with sourceCategory)
182
- - /hot-functions/application.json — **START HERE**: Hot functions from application code only.
183
- Each entry includes a \`sourceSnippet\` (lines around the hot line) and \`workspacePath\`
184
- (path to the full source file in the workspace) when source code is available.
185
- - /hot-functions/dependencies.json — Hot functions from third-party dependencies
186
- - /hot-functions/global.json — All hot functions across all categories
187
- - /scripts/application.json — Per-script time breakdown for application code
188
- - /scripts/dependencies.json — Per-script time breakdown for dependencies
189
- - /listener-tracking.json — (optional) Event listener tracking data captured from
190
- worker processes. Contains per-event-type add/remove counts for EventTarget and
191
- EventEmitter, plus exceedances where listener counts exceeded maxListeners.
192
- - /src/index.json — Mapping of source files to their hot functions (quick overview
193
- of which files matter and what bottlenecks they contain)
194
- - /tests/<relative-path> — Test source files (directory structure preserved)
195
- - /src/<relative-path> — Application and dependency source files referenced by
196
- hot functions (directory structure preserved from the project root)
5
+ // src/cli.ts
6
+ import yargs from "yargs";
7
+ import { hideBin } from "yargs/helpers";
197
8
 
198
- ## Your workflow
9
+ // src/models/init.ts
10
+ import { ChatOpenAI } from "@langchain/openai";
11
+ import { ChatAnthropic } from "@langchain/anthropic";
12
+ function initModel() {
13
+ const modelOverride = process.env.ZEITZEUGE_MODEL;
14
+ const openaiKey = process.env.OPENAI_API_KEY;
15
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
16
+ if (openaiKey) {
17
+ return new ChatOpenAI({
18
+ model: modelOverride ?? "gpt-5.2",
19
+ apiKey: openaiKey
20
+ });
21
+ }
22
+ if (anthropicKey) {
23
+ return new ChatAnthropic({
24
+ model: modelOverride ?? "claude-opus-4-6",
25
+ apiKey: anthropicKey
26
+ });
27
+ }
28
+ throw new Error(`No API key found. Set OPENAI_API_KEY or ANTHROPIC_API_KEY in your environment.
199
29
 
200
- 1. Read /hot-functions/application.json FIRST — these are the application-level
201
- bottlenecks the developer wants to optimize
202
- 2. Read /scripts/application.json for the per-file view of application code time
203
- 3. Read /hot-functions/dependencies.json for costly dependency calls
204
- 4. If present, read /heap-profiles/index.json and /heap-profiles/<file>.json to identify
205
- allocation hotspots (functions/scripts allocating lots of bytes)
206
- 5. If present, read /listener-tracking.json for event listener add/remove patterns
207
- and listener exceedances (too many listeners on a single target)
208
- 6. Read /summary.json and /timing/overview.json for the big picture
209
- 7. Read CPU profiles in /profiles/ for detailed call trees of the slowest tests
210
- 8. Read the actual source code in /src/ and /tests/ to understand root causes
211
- 9. Provide specific, actionable fixes targeting the application code
30
+ ` + ` export OPENAI_API_KEY=sk-...
31
+ ` + ` # or
32
+ ` + ` export ANTHROPIC_API_KEY=sk-ant-...
33
+ `);
34
+ }
212
35
 
213
- ## What to look for
36
+ // src/browser/launch.ts
37
+ import { remote } from "webdriverio";
38
+ async function launchBrowser(options = {}) {
39
+ const { headless = true } = options;
40
+ const browser = await remote({
41
+ capabilities: {
42
+ browserName: "chrome",
43
+ "goog:chromeOptions": {
44
+ args: [
45
+ ...headless ? ["--headless=new"] : [],
46
+ "--no-sandbox",
47
+ "--disable-gpu",
48
+ "--disable-dev-shm-usage"
49
+ ]
50
+ }
51
+ },
52
+ logLevel: "warn"
53
+ });
54
+ return browser;
55
+ }
56
+ async function closeBrowser(browser) {
57
+ try {
58
+ await browser.deleteSession();
59
+ } catch {}
60
+ }
214
61
 
215
- ### Application code bottlenecks (PRIMARY FOCUS)
216
- - Functions with high self time — where is the application spending CPU?
217
- - Expensive algorithms: O(n²) loops, unnecessary sorting, repeated work
218
- - String/JSON operations: excessive serialization, string concatenation in loops
219
- - Object allocation hotspots: functions creating many short-lived objects
220
- - Synchronous blocking: file I/O, crypto, or compression in hot paths
221
- - Redundant computation: values computed repeatedly that could be cached/memoized
222
- - Data structure choices: using arrays where Maps/Sets would be O(1)
223
-
224
- ### Dependency-related bottlenecks
225
- - Dependencies consuming disproportionate CPU — suggest alternatives or configuration
226
- - Unnecessary calls to expensive dependency APIs in hot paths
227
- - Dependencies pulled in for simple operations that could be hand-written
228
-
229
- ### GC pressure from application code
230
- - Application functions creating many temporary objects in tight loops
231
- - Large array/object allocations that could be pooled or reused
232
- - Closures capturing large scopes unnecessarily
233
-
234
- ### Allocation hotspots (from heap profiles, if present)
235
- - Functions allocating a large share of total bytes (even if CPU isn't dominant)
236
- - Scripts/modules responsible for most allocation — suggest caching, reuse, pooling,
237
- or avoiding intermediate arrays/objects
238
- - When allocation hotspots match CPU hotspots, prioritize fixes there first
239
-
240
- ### Call chain analysis
241
- - Each hot function includes a \`callerChain\` field — the chain of callers
242
- from the hot function up toward the entry point. Use this to understand
243
- WHY a function is hot and which application-level call triggers it.
244
- - Trace expensive call trees to find which APPLICATION function triggers them
245
- - Follow the call tree from application entry points down to the hot leaf functions
246
- - Identify which application-level design decisions lead to the bottleneck
247
-
248
- ### Event listener tracking (from /listener-tracking.json, if present)
249
- - **Exceedances** — when a single EventTarget or EventEmitter accumulates more
250
- listeners than its maxListeners threshold (default 10), this is a strong
251
- signal of a listener leak. The exceedance data includes the target type
252
- (e.g. AbortSignal), event name, listener count, and a stack trace snippet
253
- pointing to the code that registered the excess listener.
254
- - **Add/remove imbalances** — when \`addCount\` significantly exceeds
255
- \`removeCount\` for a given event type, listeners are being registered but
256
- not cleaned up. This causes memory growth and eventually GC pressure.
257
- Look for patterns like:
258
- - AbortSignal abort listeners not cleaned up (common with fetch/streams)
259
- - EventEmitter listeners added in loops without corresponding removal
260
- - Missing \`{ once: true }\` option or \`AbortController\` cleanup
261
- - When exceedances or imbalances are found, read the source code to identify
262
- the root cause and suggest specific fixes (e.g. using \`AbortController\`,
263
- \`removeEventListener\`, \`{ once: true }\`, or restructuring listener
264
- registration to avoid accumulation).
265
-
266
- ### Test infrastructure (SECONDARY — only if impactful)
267
- - Test setup creating artificial overhead that dwarfs application execution
268
- - Benchmarks measuring setup cost instead of application performance
269
- - Only mention if it prevents getting clean application performance data
270
-
271
- ## Finding categories
272
-
273
- Each finding MUST use one of these exact category values:
274
-
275
- - **algorithm** — Inefficient algorithm: O(n²) loops, brute-force search, repeated work
276
- - **serialization** — Excessive JSON.stringify/parse, string concatenation, encoding
277
- - **allocation** — Excessive object/array creation causing GC pressure
278
- - **event-handling** — Listener leaks, unbounded event handler accumulation
279
- - **hot-function** — Generic CPU-hot function that doesn't fit a more specific category
280
- - **gc-pressure** — High garbage collection overhead
281
- - **listener-leak** — Event listeners not cleaned up properly
282
- - **unnecessary-computation** — Redundant work that could be cached or eliminated
283
- - **blocking-io** — Synchronous I/O or blocking operations in hot paths
284
- - **dependency-bottleneck** — Expensive dependency call the developer can optimize
285
- - **slow-test** — Test itself is slow due to setup or teardown
286
- - **expensive-setup** — Costly test setup (beforeAll/beforeEach)
287
- - **import-overhead** — Expensive module imports at test time
288
- - **other** — Doesn't fit any of the above
289
-
290
- Prefer more specific categories (algorithm, serialization, allocation, event-handling)
291
- over generic ones (hot-function, other) when the root cause is clear.
292
-
293
- ## Severity classification
294
-
295
- Assign severity based on measured impact — do NOT guess:
296
-
297
- - **critical** — A single function consuming >15% self-time, listener exceedances
298
- (count exceeding maxListeners), or GC overhead >10% of total profile duration
299
- - **warning** — A function consuming 5–15% self-time, listener add/remove imbalance
300
- where addCount > 2× removeCount, or GC overhead between 5–10%
301
- - **info** — A function consuming <5% self-time, minor inefficiencies,
302
- dependency observations, or small optimisation opportunities
303
-
304
- Always base severity on the actual numbers from the profiling data — never inflate.
305
-
306
- ## Verification rules
307
-
308
- These rules are mandatory for every finding:
309
-
310
- 1. **ALWAYS read the source file** in /src/ or /tests/ and verify the code
311
- BEFORE suggesting a fix. Never suggest a fix for code you have not read.
312
- 2. **Never guess at line numbers** — confirm by reading the file. If the profile
313
- reports line 42 but the source at line 42 doesn't match, say so.
314
- 3. Each \`suggestedFix\` MUST reference the actual current code and describe
315
- what to change. Include a before/after snippet from the real source.
316
- 4. If /hot-functions/application.json is empty or every function is <1%
317
- self-time, state that the application code is efficient — do NOT
318
- manufacture findings.
319
- 5. Never report a finding based solely on a function name — always read the
320
- implementation to confirm the issue exists.
321
-
322
- ## Cross-referencing data
323
-
324
- - When a function appears in BOTH /hot-functions/ (CPU hotspot) AND
325
- /heap-profiles/ (allocation hotspot), prioritise it and mention both
326
- dimensions in the finding.
327
- - Cross-reference /hot-functions/ data with /heap-profiles/ when both are
328
- present to find functions that are expensive in both CPU and memory.
329
- - Check whether hot dependency calls originate from application code by
330
- tracing the call tree in /profiles/.
331
- - When /listener-tracking.json is present, cross-reference exceedance stack
332
- traces with the source code in /src/ to pinpoint the registration site.
333
- - /metrics/current.json contains pre-computed aggregate metrics (suite totals,
334
- CPU category breakdown, top hot functions). Use it for the big-picture
335
- numbers when sizing impact.
336
-
337
- ## Estimating impactMs
338
-
339
- Every finding should include an \`impactMs\` estimate when possible:
340
-
341
- - Use the hot function's \`selfTime\` as the baseline cost.
342
- - Estimate what fraction of that cost the fix would eliminate
343
- (e.g. an O(n²) → O(n) fix on data of size 1000 might eliminate ~99%).
344
- - impactMs = selfTime × estimated fraction eliminated.
345
- - Example: a function with selfTime 200ms in a 1000ms run, where an
346
- algorithm fix would remove ~80% of the work → impactMs ≈ 160.
347
- - If you cannot reasonably estimate the savings, omit impactMs rather than
348
- guessing.
349
-
350
- ## Output guidelines
351
-
352
- - Report 3–7 findings, ordered by impact ON THE APPLICATION CODE
353
- - Focus findings on functions the developer CAN change (application code first,
354
- then dependency usage patterns, then test structure)
355
- - Be specific — name actual files, functions, line numbers from the source code
356
- - Provide concrete code-level fixes, not generic advice
357
- - When reporting a dependency bottleneck, explain what application code is
358
- calling it and how the developer can reduce that cost
359
- - If the application code is already efficient, say so — don't force findings
360
- about test infrastructure just to fill the report
361
-
362
- ## Structured output fields
363
-
364
- For each finding, fill in as many fields as applicable:
365
-
366
- - \`sourceFile\` — the workspace path (e.g. /src/utils/parser.ts) or original
367
- file path where the issue occurs. Always set this when you can identify
368
- the file.
369
- - \`lineNumber\` — the 1-based line number in the source file. Only set after
370
- verifying by reading the file.
371
- - \`confidence\` — \`high\` if you read the source and confirmed the issue,
372
- \`medium\` if the profiling data strongly suggests it but you couldn't fully
373
- verify, \`low\` if inferred from patterns.
374
- - \`estimatedSavingsMs\` — your estimate of time saved if the fix is applied.
375
- - \`beforeCode\` — a snippet of the CURRENT problematic code, copied from the
376
- source file you read. Keep it focused (5–15 lines).
377
- - \`afterCode\` — the IMPROVED code snippet showing the fix. Must be a drop-in
378
- replacement for \`beforeCode\`.
379
- - \`affectedTests\` — list of test names that exercise this code path and would
380
- benefit from the fix.
381
- - \`impactMs\` — the current measured cost (e.g. selfTime of the hot function).`;
382
-
383
- // src/schema.ts
384
- import { z } from "zod";
385
- var ALL_CATEGORIES, FindingSchema, FindingsSchema;
386
- var init_schema = __esm(() => {
387
- ALL_CATEGORIES = [
388
- "memory-leak",
389
- "large-retained-object",
390
- "detached-dom",
391
- "render-blocking",
392
- "long-task",
393
- "unused-code",
394
- "waterfall-bottleneck",
395
- "large-asset",
396
- "frame-blocking-function",
397
- "listener-leak",
398
- "gc-pressure",
399
- "slow-test",
400
- "expensive-setup",
401
- "hot-function",
402
- "unnecessary-computation",
403
- "import-overhead",
404
- "dependency-bottleneck",
405
- "algorithm",
406
- "serialization",
407
- "allocation",
408
- "event-handling",
409
- "blocking-io",
410
- "other"
411
- ];
412
- FindingSchema = z.object({
413
- severity: z.enum(["critical", "warning", "info"]),
414
- title: z.string().describe("Short title for the finding"),
415
- description: z.string().describe("Detailed explanation of the issue"),
416
- category: z.string().describe(`Category of the performance issue. Use one of: ${ALL_CATEGORIES.join(", ")}`),
417
- resourceUrl: z.string().optional().describe("URL of the resource involved"),
418
- workspacePath: z.string().optional().describe("Path in the VFS workspace"),
419
- impactMs: z.number().optional().describe("Estimated current cost in ms (e.g. selfTime of the hot function)"),
420
- estimatedSavingsMs: z.number().optional().describe("Estimated time savings in ms if the fix is applied. Computed as impactMs × fraction eliminated by the fix."),
421
- confidence: z.enum(["high", "medium", "low"]).optional().describe("How confident you are in this finding. high = verified in source code, medium = strong signal but partial verification, low = inferred from data patterns"),
422
- retainedSize: z.number().optional().describe("Retained heap size in bytes"),
423
- retainerPath: z.array(z.string()).optional().describe("Object retention path in the heap"),
424
- sourceFile: z.string().optional().describe("Primary source file where the issue occurs (workspace path or original path)"),
425
- lineNumber: z.number().optional().describe("Line number in the source file where the issue occurs (1-based)"),
426
- suggestedFix: z.string().describe("Code snippet or guidance to fix the issue"),
427
- beforeCode: z.string().optional().describe("The current problematic code snippet from the source file"),
428
- afterCode: z.string().optional().describe("The improved code snippet that fixes the issue"),
429
- testFile: z.string().optional().describe("Test file path (for test performance findings)"),
430
- affectedTests: z.array(z.string()).optional().describe("Test names that would benefit from this fix (for test performance findings)"),
431
- hotFunction: z.object({
432
- name: z.string(),
433
- scriptUrl: z.string(),
434
- lineNumber: z.number(),
435
- selfTime: z.number(),
436
- selfPercent: z.number()
437
- }).optional().describe("Hot function details (for hot-function findings)")
438
- });
439
- FindingsSchema = z.object({
440
- findings: z.array(FindingSchema)
441
- });
442
- });
443
-
444
- // src/output/progress.ts
445
- class TodoProgressRenderer {
446
- spinner;
447
- lastStatusByKey = new Map;
448
- lastInProgressKey;
449
- baseSpinnerText;
450
- printedHeader = false;
451
- constructor(spinner) {
452
- this.spinner = spinner;
453
- this.baseSpinnerText = spinner.text;
454
- }
455
- printHeaderOnce() {
456
- if (this.printedHeader)
457
- return;
458
- this.printedHeader = true;
459
- const header = "Performance analysis progress:";
460
- this.spinner.stopAndPersist({ symbol: " ", text: header });
461
- this.spinner.start();
62
+ // src/browser/runtime-trace.ts
63
+ var SCRIPTING_EVENTS = new Set([
64
+ "FunctionCall",
65
+ "EvaluateScript",
66
+ "TimerFire",
67
+ "RequestAnimationFrame",
68
+ "FireAnimationFrame"
69
+ ]);
70
+ var LAYOUT_EVENTS = new Set(["Layout", "UpdateLayoutTree", "RecalculateStyles"]);
71
+ var PAINTING_EVENTS = new Set(["Paint", "CompositeLayers", "RasterTask"]);
72
+ var GC_EVENT_NAMES = new Set(["MajorGC", "MinorGC"]);
73
+ var BLOCKING_EVENT_NAMES = new Set(["FunctionCall", "EvaluateScript"]);
74
+ var BLOCKING_THRESHOLD_US = 50000;
75
+ function parseRuntimeTrace(traceEvents, navigationStartTs) {
76
+ if (traceEvents.length === 0) {
77
+ return emptyRuntimeTrace();
462
78
  }
463
- handleChunk(chunk) {
464
- const todos = extractTodosFromStreamChunk(chunk);
465
- if (!todos)
466
- return;
467
- for (const todo of todos) {
468
- const key = todo.id && String(todo.id) || todo.content;
469
- const prevStatus = this.lastStatusByKey.get(key);
470
- const nextStatus = todo.status;
471
- if (prevStatus !== nextStatus) {
472
- this.lastStatusByKey.set(key, nextStatus);
473
- if (nextStatus === "completed" && prevStatus !== "completed") {
474
- this.printHeaderOnce();
475
- this.spinner.stopAndPersist({ symbol: " ", text: ` ✓ ${todo.content}` });
476
- this.spinner.start();
477
- }
478
- if (nextStatus === "in_progress" && this.lastInProgressKey !== key) {
479
- this.lastInProgressKey = key;
480
- this.printHeaderOnce();
481
- const base = this.baseSpinnerText ?? this.spinner.text;
482
- this.spinner.text = base ? `${base} (${todo.content})` : todo.content;
483
- }
484
- }
485
- }
79
+ const mainThreadId = findMainThread(traceEvents);
80
+ const mainEvents = traceEvents.filter((e) => e.tid === mainThreadId);
81
+ const blockingFunctions = extractBlockingFunctions(mainEvents, navigationStartTs);
82
+ const eventListeners = extractEventListenerInfo(mainEvents);
83
+ const frameBreakdown = buildFrameBreakdown(mainEvents);
84
+ const gcEvents = extractGCEvents(mainEvents, navigationStartTs);
85
+ const frequentEvents = findFrequentEvents(mainEvents);
86
+ let minTs = Infinity;
87
+ let maxTs = -Infinity;
88
+ for (const e of traceEvents) {
89
+ if (e.ts < minTs)
90
+ minTs = e.ts;
91
+ const endTs = e.ts + (e.dur ?? 0);
92
+ if (endTs > maxTs)
93
+ maxTs = endTs;
486
94
  }
487
- }
488
- function extractTodosFromStreamChunk(chunk) {
489
- if (!chunk || typeof chunk !== "object")
490
- return;
491
- const direct = chunk;
492
- if (Array.isArray(direct.todos))
493
- return direct.todos;
494
- for (const value of Object.values(chunk)) {
495
- if (!value || typeof value !== "object")
496
- continue;
497
- const nested = value;
498
- if (Array.isArray(nested.todos))
499
- return nested.todos;
500
- }
501
- }
502
-
503
- // src/analysis/agent.ts
504
- var exports_agent = {};
505
- __export(exports_agent, {
506
- formatBytes: () => formatBytes,
507
- analyzeTestPerformance: () => analyzeTestPerformance,
508
- analyze: () => analyze
509
- });
510
- import {
511
- createDeepAgent
512
- } from "deepagents";
513
- import { providerStrategy } from "langchain";
514
- async function invokeWithTodoStreaming(agent, userMessage, spinner) {
515
- const renderer = new TodoProgressRenderer(spinner);
516
- const stream = await agent.stream({ messages: [{ role: "user", content: userMessage }] }, { streamMode: ["updates", "values"] });
517
- let lastValues;
518
- for await (const item of stream) {
519
- if (Array.isArray(item) && item.length === 2) {
520
- const mode = item[0];
521
- const chunk = item[1];
522
- renderer.handleChunk(chunk);
523
- if (mode === "values")
524
- lastValues = chunk;
525
- continue;
526
- }
527
- renderer.handleChunk(item);
528
- lastValues = item;
529
- }
530
- return lastValues;
531
- }
532
- function buildPageLoadUserMessage(ctx) {
533
- const { url, heapSummary, traceResult } = ctx;
534
- const m = traceResult.metrics;
535
- const reqCount = traceResult.networkRequests.length;
536
- const renderBlocking = traceResult.networkRequests.filter((r) => r.isRenderBlocking).length;
537
- const totalTransfer = traceResult.networkRequests.reduce((s, r) => s + r.encodedSize, 0);
538
- const hasRuntime = !!traceResult.runtimeTrace;
539
- const lines = [
540
- "Analyze the frontend performance data in this workspace.",
541
- "",
542
- `URL: ${url}`,
543
- `Page load: ${Math.round(m.loadComplete)}ms | FCP: ${Math.round(m.firstContentfulPaint)}ms | LCP: ${Math.round(m.largestContentfulPaint)}ms | TBT: ${Math.round(m.totalBlockingTime)}ms`,
544
- `Heap: ${formatBytes(heapSummary.metadata.totalSize)} total, ${heapSummary.metadata.nodeCount.toLocaleString()} nodes, ${heapSummary.detachedNodes.count} detached DOM nodes`,
545
- `Network: ${reqCount} requests, ${formatBytes(totalTransfer)} transferred, ${renderBlocking} render-blocking`,
546
- `Long tasks: ${m.longTasks.length}`
547
- ];
548
- if (hasRuntime) {
549
- const rt = traceResult.runtimeTrace;
550
- lines.push(`Runtime trace: ${rt.blockingFunctions.length} blocking functions, ${rt.gcEvents.length} GC events (${Math.round(rt.gcEvents.reduce((s, e) => s + e.duration, 0))}ms total)`);
551
- }
552
- lines.push("", "Available data:", "- /heap/summary.json — parsed heap snapshot", "- /trace/summary.json — page load metrics", "- /trace/network-waterfall.json — request timing and sizes", "- /trace/asset-manifest.json — index of stored assets");
553
- if (hasRuntime) {
554
- lines.push("- /trace/runtime/summary.json — runtime trace overview", "- /trace/runtime/blocking-functions.json — main thread blocking functions", "- /trace/runtime/event-listeners.json — listener add/remove counts", "- /trace/runtime/frame-breakdown.json — scripting vs layout vs paint vs GC", "- /trace/runtime/raw-events.json — full Chrome trace events");
555
- }
556
- lines.push("- /scripts/ — JavaScript source files", "- /styles/ — CSS source files", "- /html/document.html — page markup", "", "Explore the workspace, read source files to verify root causes, and provide code-level fixes.");
557
- return lines.join(`
558
- `);
559
- }
560
- async function analyze(model, backend, spinner, context) {
561
- const agent = createDeepAgent({
562
- model,
563
- systemPrompt: SYSTEM_PROMPT,
564
- backend,
565
- responseFormat: providerStrategy(FindingsSchema)
566
- });
567
- const userMessage = context ? buildPageLoadUserMessage(context) : [
568
- "Analyze the frontend performance data in this workspace.",
569
- "",
570
- "Start by reading /heap/summary.json, /trace/summary.json, and /trace/runtime/summary.json",
571
- "to understand the overall picture, then explore source files to verify root causes."
572
- ].join(`
573
- `);
574
- const result = await invokeWithTodoStreaming(agent, userMessage, spinner);
575
- const findings = result.structuredResponse.findings;
576
- if (!Array.isArray(findings)) {
577
- throw new Error("Agent did not return structured findings.");
578
- }
579
- return findings;
580
- }
581
- function buildVitestUserMessage(ctx) {
582
- const { metrics, hasHeapProfiles, hasListenerTracking } = ctx;
583
- const lines = [
584
- "Analyze the performance of the APPLICATION CODE being tested in this Vitest workspace.",
585
- "",
586
- `Test suite: ${metrics.suite.totalTests} tests, total duration ${metrics.suite.totalDuration}ms`,
587
- `CPU breakdown: application ${metrics.cpu.applicationPercent}%, dependencies ${metrics.cpu.dependencyPercent}%, GC ${metrics.cpu.gcPercentage}%, idle ${metrics.cpu.idlePercentage}%`,
588
- `Slowest file: ${metrics.suite.slowestFile} (${metrics.suite.slowestFileDuration}ms)`,
589
- `Slowest test: ${metrics.suite.slowestTestName} (${metrics.suite.slowestTestDuration}ms)`,
590
- "",
591
- "Available data:",
592
- "- /hot-functions/application.json — application-level CPU hotspots",
593
- "- /scripts/application.json — per-file CPU time for application code",
594
- "- /hot-functions/dependencies.json — dependency CPU hotspots",
595
- "- /scripts/dependencies.json — per-file CPU time for dependencies"
596
- ];
597
- if (hasListenerTracking) {
598
- lines.push("- /listener-tracking.json — event listener add/remove patterns and exceedances");
599
- }
600
- if (hasHeapProfiles) {
601
- lines.push("- /heap-profiles/index.json — heap profile manifest", "- /heap-profiles/<file>.json — per-file allocation hotspots");
602
- }
603
- lines.push("- /summary.json — overall test run summary", "- /timing/overview.json — per-file test durations", "- /timing/slow-tests.json — tests exceeding the slow threshold", "- /profiles/ — full CPU profile summaries with call trees", "- /metrics/current.json — pre-computed aggregate metrics", "- /src/ and /tests/ — source files", "", "Focus findings on the APPLICATION code — what can the developer change in their own", "codebase to improve performance? Read source files to verify root causes before suggesting fixes.");
604
- return lines.join(`
605
- `);
606
- }
607
- async function analyzeTestPerformance(model, backend, spinner, context) {
608
- const agent = createDeepAgent({
609
- model,
610
- systemPrompt: VITEST_SYSTEM_PROMPT,
611
- backend,
612
- responseFormat: providerStrategy(FindingsSchema)
613
- });
614
- const userMessage = context ? buildVitestUserMessage(context) : [
615
- "Analyze the performance of the APPLICATION CODE being tested in this Vitest workspace.",
616
- "",
617
- "Start with /hot-functions/application.json, then explore source files to verify",
618
- "root causes and provide code-level fixes."
619
- ].join(`
620
- `);
621
- const result = await invokeWithTodoStreaming(agent, userMessage, spinner);
622
- const findings = result.structuredResponse?.findings;
623
- if (!Array.isArray(findings)) {
624
- throw new Error(`Failed to analyze test performance: ${result.messages.at(-1)?.text}`);
625
- }
626
- return findings;
627
- }
628
- function formatBytes(bytes) {
629
- if (bytes < 1024)
630
- return `${bytes} B`;
631
- if (bytes < 1024 * 1024)
632
- return `${(bytes / 1024).toFixed(1)} KB`;
633
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
634
- }
635
- var init_agent = __esm(() => {
636
- init_schema();
637
- });
638
-
639
- // src/cli.ts
640
- import yargs from "yargs";
641
- import { hideBin } from "yargs/helpers";
642
-
643
- // src/models/init.ts
644
- import { ChatOpenAI } from "@langchain/openai";
645
- import { ChatAnthropic } from "@langchain/anthropic";
646
- function initModel() {
647
- const modelOverride = process.env.ZEITZEUGE_MODEL;
648
- const openaiKey = process.env.OPENAI_API_KEY;
649
- const anthropicKey = process.env.ANTHROPIC_API_KEY;
650
- if (openaiKey) {
651
- return new ChatOpenAI({
652
- model: modelOverride ?? "gpt-5.2",
653
- apiKey: openaiKey
654
- });
655
- }
656
- if (anthropicKey) {
657
- return new ChatAnthropic({
658
- model: modelOverride ?? "claude-opus-4-6",
659
- apiKey: anthropicKey
660
- });
661
- }
662
- throw new Error(`No API key found. Set OPENAI_API_KEY or ANTHROPIC_API_KEY in your environment.
663
-
664
- ` + ` export OPENAI_API_KEY=sk-...
665
- ` + ` # or
666
- ` + ` export ANTHROPIC_API_KEY=sk-ant-...
667
- `);
668
- }
669
-
670
- // src/browser/launch.ts
671
- import { remote } from "webdriverio";
672
- async function launchBrowser(options = {}) {
673
- const { headless = true } = options;
674
- const browser = await remote({
675
- capabilities: {
676
- browserName: "chrome",
677
- "goog:chromeOptions": {
678
- args: [
679
- ...headless ? ["--headless=new"] : [],
680
- "--no-sandbox",
681
- "--disable-gpu",
682
- "--disable-dev-shm-usage"
683
- ]
684
- }
685
- },
686
- logLevel: "warn"
687
- });
688
- return browser;
689
- }
690
- async function closeBrowser(browser) {
691
- try {
692
- await browser.deleteSession();
693
- } catch {}
694
- }
695
-
696
- // src/browser/runtime-trace.ts
697
- var SCRIPTING_EVENTS = new Set([
698
- "FunctionCall",
699
- "EvaluateScript",
700
- "TimerFire",
701
- "RequestAnimationFrame",
702
- "FireAnimationFrame"
703
- ]);
704
- var LAYOUT_EVENTS = new Set(["Layout", "UpdateLayoutTree", "RecalculateStyles"]);
705
- var PAINTING_EVENTS = new Set(["Paint", "CompositeLayers", "RasterTask"]);
706
- var GC_EVENT_NAMES = new Set(["MajorGC", "MinorGC"]);
707
- var BLOCKING_EVENT_NAMES = new Set(["FunctionCall", "EvaluateScript"]);
708
- var BLOCKING_THRESHOLD_US = 50000;
709
- function parseRuntimeTrace(traceEvents, navigationStartTs) {
710
- if (traceEvents.length === 0) {
711
- return emptyRuntimeTrace();
712
- }
713
- const mainThreadId = findMainThread(traceEvents);
714
- const mainEvents = traceEvents.filter((e) => e.tid === mainThreadId);
715
- const blockingFunctions = extractBlockingFunctions(mainEvents, navigationStartTs);
716
- const eventListeners = extractEventListenerInfo(mainEvents);
717
- const frameBreakdown = buildFrameBreakdown(mainEvents);
718
- const gcEvents = extractGCEvents(mainEvents, navigationStartTs);
719
- const frequentEvents = findFrequentEvents(mainEvents);
720
- let minTs = Infinity;
721
- let maxTs = -Infinity;
722
- for (const e of traceEvents) {
723
- if (e.ts < minTs)
724
- minTs = e.ts;
725
- const endTs = e.ts + (e.dur ?? 0);
726
- if (endTs > maxTs)
727
- maxTs = endTs;
728
- }
729
- const traceDuration = (maxTs - minTs) / 1000;
730
- return {
731
- totalEvents: traceEvents.length,
732
- mainThreadId,
733
- traceDuration,
734
- frameBreakdown,
735
- blockingFunctions,
736
- eventListeners,
737
- gcEvents,
738
- frequentEvents
739
- };
95
+ const traceDuration = (maxTs - minTs) / 1000;
96
+ return {
97
+ totalEvents: traceEvents.length,
98
+ mainThreadId,
99
+ traceDuration,
100
+ frameBreakdown,
101
+ blockingFunctions,
102
+ eventListeners,
103
+ gcEvents,
104
+ frequentEvents
105
+ };
740
106
  }
741
107
  function findMainThread(events) {
742
108
  const metadata = events.find((e) => e.cat === "__metadata" && e.name === "thread_name" && e.args?.name === "CrRendererMain");
@@ -1107,313 +473,923 @@ async function tracePageLoad(cdpSession, _options = {}) {
1107
473
  };
1108
474
  }
1109
475
 
1110
- // src/browser/capture.ts
1111
- async function capturePage(browser, url, options = {}) {
1112
- const { timeout = 30000 } = options;
1113
- const puppeteerBrowser = await browser.getPuppeteer();
1114
- const pages = await puppeteerBrowser.pages();
1115
- const page = pages[0];
1116
- if (!page) {
1117
- throw new Error("No page found in Puppeteer browser");
1118
- }
1119
- const cdpSession = await page.createCDPSession();
1120
- const traceHandle = await tracePageLoad(cdpSession, options);
1121
- await page.goto(url, {
1122
- waitUntil: "load",
1123
- timeout
1124
- });
1125
- await new Promise((r) => setTimeout(r, 2000));
1126
- const traceResult = await traceHandle.stop();
1127
- await cdpSession.send("HeapProfiler.enable");
1128
- const chunks = [];
1129
- cdpSession.on("HeapProfiler.addHeapSnapshotChunk", (params) => {
1130
- chunks.push(params.chunk);
1131
- });
1132
- await cdpSession.send("HeapProfiler.collectGarbage");
1133
- await cdpSession.send("HeapProfiler.takeHeapSnapshot", {
1134
- reportProgress: false
1135
- });
1136
- await cdpSession.send("HeapProfiler.disable");
1137
- await cdpSession.detach();
1138
- return {
1139
- heapSnapshot: {
1140
- data: chunks.join(""),
1141
- capturedAt: Date.now(),
1142
- url
1143
- },
1144
- trace: traceResult
1145
- };
1146
- }
476
+ // src/browser/capture.ts
477
+ async function capturePage(browser, url, options = {}) {
478
+ const { timeout = 30000 } = options;
479
+ const puppeteerBrowser = await browser.getPuppeteer();
480
+ const pages = await puppeteerBrowser.pages();
481
+ const page = pages[0];
482
+ if (!page) {
483
+ throw new Error("No page found in Puppeteer browser");
484
+ }
485
+ const cdpSession = await page.createCDPSession();
486
+ const traceHandle = await tracePageLoad(cdpSession, options);
487
+ await page.goto(url, {
488
+ waitUntil: "load",
489
+ timeout
490
+ });
491
+ await new Promise((r) => setTimeout(r, 2000));
492
+ const traceResult = await traceHandle.stop();
493
+ await cdpSession.send("HeapProfiler.enable");
494
+ const chunks = [];
495
+ cdpSession.on("HeapProfiler.addHeapSnapshotChunk", (params) => {
496
+ chunks.push(params.chunk);
497
+ });
498
+ await cdpSession.send("HeapProfiler.collectGarbage");
499
+ await cdpSession.send("HeapProfiler.takeHeapSnapshot", {
500
+ reportProgress: false
501
+ });
502
+ await cdpSession.send("HeapProfiler.disable");
503
+ await cdpSession.detach();
504
+ return {
505
+ heapSnapshot: {
506
+ data: chunks.join(""),
507
+ capturedAt: Date.now(),
508
+ url
509
+ },
510
+ trace: traceResult
511
+ };
512
+ }
513
+
514
+ // src/analysis/parser.ts
515
+ function parseSnapshot(rawSnapshot) {
516
+ const v8 = JSON.parse(rawSnapshot.data);
517
+ const meta = v8.snapshot.meta;
518
+ const nodeFieldCount = meta.node_fields.length;
519
+ const edgeFieldCount = meta.edge_fields.length;
520
+ const nodeTypes = meta.node_types[0];
521
+ const edgeTypes = meta.edge_types[0];
522
+ const nodeTypeIdx = meta.node_fields.indexOf("type");
523
+ const nodeNameIdx = meta.node_fields.indexOf("name");
524
+ const nodeIdIdx = meta.node_fields.indexOf("id");
525
+ const nodeSelfSizeIdx = meta.node_fields.indexOf("self_size");
526
+ const nodeEdgeCountIdx = meta.node_fields.indexOf("edge_count");
527
+ const nodeDetachednessIdx = meta.node_fields.indexOf("detachedness");
528
+ const edgeTypeIdx = meta.edge_fields.indexOf("type");
529
+ const edgeNameIdx = meta.edge_fields.indexOf("name_or_index");
530
+ const edgeToNodeIdx = meta.edge_fields.indexOf("to_node");
531
+ const nodeCount = v8.snapshot.node_count;
532
+ const nodes = [];
533
+ let edgeOffset = 0;
534
+ let totalSize = 0;
535
+ for (let i = 0;i < nodeCount; i++) {
536
+ const base = i * nodeFieldCount;
537
+ const selfSize = v8.nodes[base + nodeSelfSizeIdx] ?? 0;
538
+ totalSize += selfSize;
539
+ nodes.push({
540
+ ordinal: i,
541
+ type: nodeTypes[v8.nodes[base + nodeTypeIdx] ?? 0] ?? "unknown",
542
+ name: v8.strings[v8.nodes[base + nodeNameIdx] ?? 0] ?? "",
543
+ id: v8.nodes[base + nodeIdIdx] ?? 0,
544
+ selfSize,
545
+ edgeCount: v8.nodes[base + nodeEdgeCountIdx] ?? 0,
546
+ detachedness: nodeDetachednessIdx >= 0 ? v8.nodes[base + nodeDetachednessIdx] ?? 0 : 0,
547
+ edgeStartIndex: edgeOffset
548
+ });
549
+ edgeOffset += (v8.nodes[base + nodeEdgeCountIdx] ?? 0) * edgeFieldCount;
550
+ }
551
+ const adjacency = Array.from({ length: nodeCount }, () => []);
552
+ for (let i = 0;i < nodeCount; i++)
553
+ adjacency[i] = [];
554
+ const reverseAdj = Array.from({ length: nodeCount }, () => []);
555
+ for (let i = 0;i < nodeCount; i++)
556
+ reverseAdj[i] = [];
557
+ for (let i = 0;i < nodeCount; i++) {
558
+ const node = nodes[i];
559
+ for (let e = 0;e < node.edgeCount; e++) {
560
+ const edgeBase = node.edgeStartIndex + e * edgeFieldCount;
561
+ const edgeTypeVal = edgeTypes[v8.edges[edgeBase + edgeTypeIdx] ?? 0];
562
+ if (edgeTypeVal === "weak")
563
+ continue;
564
+ const toNodeArrayIdx = v8.edges[edgeBase + edgeToNodeIdx] ?? 0;
565
+ const toOrdinal = toNodeArrayIdx / nodeFieldCount;
566
+ if (toOrdinal >= 0 && toOrdinal < nodeCount) {
567
+ adjacency[i].push(toOrdinal);
568
+ reverseAdj[toOrdinal].push(i);
569
+ }
570
+ }
571
+ }
572
+ const visited = new Uint8Array(nodeCount);
573
+ const bfsOrder = [];
574
+ const queue = [0];
575
+ visited[0] = 1;
576
+ while (queue.length > 0) {
577
+ const curr = queue.shift();
578
+ bfsOrder.push(curr);
579
+ for (const next of adjacency[curr]) {
580
+ if (!visited[next]) {
581
+ visited[next] = 1;
582
+ queue.push(next);
583
+ }
584
+ }
585
+ }
586
+ const dominators = new Int32Array(nodeCount).fill(-1);
587
+ dominators[0] = 0;
588
+ const nodeToRpo = new Int32Array(nodeCount).fill(-1);
589
+ for (let i = 0;i < bfsOrder.length; i++) {
590
+ nodeToRpo[bfsOrder[i]] = i;
591
+ }
592
+ function intersect(b1Init, b2Init) {
593
+ let b1 = b1Init;
594
+ let b2 = b2Init;
595
+ let finger1 = nodeToRpo[b1];
596
+ let finger2 = nodeToRpo[b2];
597
+ while (finger1 !== finger2) {
598
+ while (finger1 > finger2) {
599
+ b1 = dominators[b1];
600
+ if (b1 < 0)
601
+ return 0;
602
+ finger1 = nodeToRpo[b1];
603
+ }
604
+ while (finger2 > finger1) {
605
+ b2 = dominators[b2];
606
+ if (b2 < 0)
607
+ return 0;
608
+ finger2 = nodeToRpo[b2];
609
+ }
610
+ }
611
+ return b1;
612
+ }
613
+ let changed = true;
614
+ while (changed) {
615
+ changed = false;
616
+ for (let i = 1;i < bfsOrder.length; i++) {
617
+ const node = bfsOrder[i];
618
+ let newIdom = -1;
619
+ for (const pred of reverseAdj[node]) {
620
+ if (dominators[pred] < 0)
621
+ continue;
622
+ if (newIdom < 0) {
623
+ newIdom = pred;
624
+ } else {
625
+ newIdom = intersect(newIdom, pred);
626
+ }
627
+ }
628
+ if (newIdom >= 0 && dominators[node] !== newIdom) {
629
+ dominators[node] = newIdom;
630
+ changed = true;
631
+ }
632
+ }
633
+ }
634
+ const retainedSizes = new Float64Array(nodeCount);
635
+ for (let i = 0;i < nodeCount; i++) {
636
+ retainedSizes[i] = nodes[i].selfSize;
637
+ }
638
+ for (let i = bfsOrder.length - 1;i > 0; i--) {
639
+ const node = bfsOrder[i];
640
+ const dom = dominators[node];
641
+ if (dom >= 0 && dom !== node) {
642
+ retainedSizes[dom] = retainedSizes[dom] + retainedSizes[node];
643
+ }
644
+ }
645
+ const indexed = bfsOrder.filter((i) => i > 0).map((i) => ({ ordinal: i, retainedSize: retainedSizes[i] })).sort((a, b) => b.retainedSize - a.retainedSize).slice(0, 50);
646
+ function getEdgeName(toOrdinal) {
647
+ const dom = dominators[toOrdinal];
648
+ if (dom < 0)
649
+ return "";
650
+ const domNode = nodes[dom];
651
+ for (let e = 0;e < domNode.edgeCount; e++) {
652
+ const edgeBase = domNode.edgeStartIndex + e * edgeFieldCount;
653
+ const toNodeArrayIdx = v8.edges[edgeBase + edgeToNodeIdx] ?? 0;
654
+ if (toNodeArrayIdx / nodeFieldCount === toOrdinal) {
655
+ const nameOrIndex = v8.edges[edgeBase + edgeNameIdx] ?? 0;
656
+ const edgeTypeVal = edgeTypes[v8.edges[edgeBase + edgeTypeIdx] ?? 0];
657
+ if (edgeTypeVal === "element")
658
+ return `[${nameOrIndex}]`;
659
+ return v8.strings[nameOrIndex] ?? String(nameOrIndex);
660
+ }
661
+ }
662
+ return "";
663
+ }
664
+ function getRetainerPath(targetOrdinal, maxDepth = 10) {
665
+ const path = [];
666
+ let current = targetOrdinal;
667
+ const pathVisited = new Set;
668
+ for (let depth = 0;depth < maxDepth; depth++) {
669
+ const node = nodes[current];
670
+ const edgeName = getEdgeName(current);
671
+ path.unshift(edgeName ? `${node.name || node.type}` : node.name || node.type);
672
+ if (current === 0)
673
+ break;
674
+ pathVisited.add(current);
675
+ const dom = dominators[current];
676
+ if (dom < 0 || dom === current || pathVisited.has(dom))
677
+ break;
678
+ current = dom;
679
+ }
680
+ return path;
681
+ }
682
+ const largestObjects = indexed.map(({ ordinal, retainedSize }) => {
683
+ const node = nodes[ordinal];
684
+ return {
685
+ name: node.name || `(${node.type})`,
686
+ type: node.type,
687
+ selfSize: node.selfSize,
688
+ retainedSize,
689
+ retainerPath: getRetainerPath(ordinal)
690
+ };
691
+ });
692
+ const typeMap = new Map;
693
+ for (const node of nodes) {
694
+ if (!visited[node.ordinal])
695
+ continue;
696
+ const existing = typeMap.get(node.type);
697
+ if (existing) {
698
+ existing.count++;
699
+ existing.totalSize += node.selfSize;
700
+ } else {
701
+ typeMap.set(node.type, { count: 1, totalSize: node.selfSize });
702
+ }
703
+ }
704
+ const typeStats = Array.from(typeMap.entries()).map(([type, stats]) => ({
705
+ type,
706
+ count: stats.count,
707
+ totalSize: stats.totalSize,
708
+ avgSize: stats.count > 0 ? Math.round(stats.totalSize / stats.count) : 0
709
+ })).sort((a, b) => b.totalSize - a.totalSize);
710
+ const ctorMap = new Map;
711
+ for (const node of nodes) {
712
+ if (!visited[node.ordinal])
713
+ continue;
714
+ if (node.type !== "object" || !node.name)
715
+ continue;
716
+ const existing = ctorMap.get(node.name);
717
+ if (existing) {
718
+ existing.count++;
719
+ existing.totalSize += node.selfSize;
720
+ } else {
721
+ ctorMap.set(node.name, { count: 1, totalSize: node.selfSize });
722
+ }
723
+ }
724
+ const constructorStats = Array.from(ctorMap.entries()).map(([ctor, stats]) => ({
725
+ constructor: ctor,
726
+ count: stats.count,
727
+ totalSize: stats.totalSize,
728
+ avgSize: stats.count > 0 ? Math.round(stats.totalSize / stats.count) : 0
729
+ })).sort((a, b) => b.totalSize - a.totalSize).slice(0, 30);
730
+ const detachedExamples = [];
731
+ let detachedCount = 0;
732
+ let detachedTotalSize = 0;
733
+ for (const node of nodes) {
734
+ if (!visited[node.ordinal])
735
+ continue;
736
+ const isDetached = nodeDetachednessIdx >= 0 && node.detachedness > 0 || node.name.includes("Detached") || node.type === "native" && /HTML\w*Element|Document|Node/.test(node.name) && node.name.includes("Detached");
737
+ if (isDetached) {
738
+ detachedCount++;
739
+ detachedTotalSize += node.selfSize;
740
+ if (detachedExamples.length < 10) {
741
+ detachedExamples.push({
742
+ name: node.name || `(${node.type})`,
743
+ retainerPath: getRetainerPath(node.ordinal)
744
+ });
745
+ }
746
+ }
747
+ }
748
+ const detachedNodes = {
749
+ count: detachedCount,
750
+ totalSize: detachedTotalSize,
751
+ examples: detachedExamples
752
+ };
753
+ const closureNodes = nodes.filter((n) => visited[n.ordinal] && n.type === "closure");
754
+ const closureTotalSize = closureNodes.reduce((sum, n) => sum + n.selfSize, 0);
755
+ const topClosures = closureNodes.sort((a, b) => b.selfSize - a.selfSize).slice(0, 20).map((n) => ({
756
+ name: n.name || "(anonymous)",
757
+ contextSize: n.selfSize,
758
+ retainerPath: getRetainerPath(n.ordinal)
759
+ }));
760
+ const closureStats = {
761
+ count: closureNodes.length,
762
+ totalSize: closureTotalSize,
763
+ topClosures
764
+ };
765
+ return {
766
+ metadata: {
767
+ url: rawSnapshot.url,
768
+ capturedAt: rawSnapshot.capturedAt,
769
+ totalSize,
770
+ nodeCount: v8.snapshot.node_count,
771
+ edgeCount: v8.snapshot.edge_count
772
+ },
773
+ largestObjects,
774
+ typeStats,
775
+ constructorStats,
776
+ detachedNodes,
777
+ closureStats
778
+ };
779
+ }
780
+
781
+ // src/analysis/agent.ts
782
+ import {
783
+ createDeepAgent
784
+ } from "deepagents";
785
+ import { providerStrategy } from "langchain";
786
+
787
+ // src/analysis/prompts.ts
788
+ var SYSTEM_PROMPT = `You are an expert web performance engineer. You have access to a virtual filesystem workspace containing captured data from a real page load: heap snapshot, network trace, and Chrome runtime trace.
789
+
790
+ ## Workspace structure
791
+
792
+ - /heap/summary.json — Parsed V8 heap snapshot: largest objects, type stats, constructor stats, detached DOM nodes, closure stats
793
+ - /trace/summary.json — Page load metrics: timing, long tasks, render-blocking resources, resource breakdown
794
+ - /trace/network-waterfall.json — Every network request with timing, size, priority, render-blocking status
795
+ - /trace/asset-manifest.json — Index of all assets with paths to stored files
796
+ - /trace/runtime/summary.json — Runtime trace overview: frame breakdown (scripting/layout/paint/GC), blocking function count, listener imbalances, GC stats
797
+ - /trace/runtime/blocking-functions.json — Functions that blocked the main thread > 50ms, with script URL, line number, call stack, and duration
798
+ - /trace/runtime/event-listeners.json — Event listener add/remove counts per event type, with source locations
799
+ - /trace/runtime/frame-breakdown.json — Time spent in scripting vs layout vs paint vs GC
800
+ - /trace/runtime/raw-events.json — Full Chrome trace events (large file — read to investigate specific function calls, layouts, GC, and event dispatches)
801
+ - /scripts/*.js — Actual JavaScript source files captured during page load
802
+ - /styles/*.css — Actual CSS source files
803
+ - /html/document.html — The HTML document
804
+
805
+ ## Your workflow
806
+
807
+ 1. Read /heap/summary.json, /trace/summary.json, AND /trace/runtime/summary.json first for the big picture
808
+ 2. Identify the highest-impact issues from all datasets
809
+ 3. For each issue, dive into the relevant source files to understand the root cause
810
+ 4. Provide specific, code-level fixes
811
+
812
+ ## What to look for
813
+
814
+ ### Memory issues (from heap data)
815
+ - Memory leaks: unbounded arrays, maps, caches that grow without bound
816
+ - Detached DOM nodes: DOM elements removed from the document but still referenced
817
+ - Large retained objects: single objects or trees retaining disproportionate memory
818
+ - Closure leaks: closures capturing variables they no longer need
819
+
820
+ ### Page-load issues (from trace + source code)
821
+ - Render-blocking scripts: <script> in <head> without async/defer — read the script to judge if it must be synchronous
822
+ - Render-blocking CSS: large stylesheets blocking first paint
823
+ - Long tasks (> 50ms): identify the function/module causing the block by reading the source
824
+ - Large bundles: scripts > 100KB — search for unused imports or code that could be lazy-loaded
825
+ - Sequential waterfalls: resources chained sequentially that could load in parallel
826
+
827
+ ### Runtime issues (from Chrome trace)
828
+ - Frame-blocking functions: read /trace/runtime/blocking-functions.json first, then inspect the actual script source at the reported line number to understand what the function does and how to optimize it
829
+ - Event listener leaks: check /trace/runtime/event-listeners.json for event types where addCount >> removeCount, then grep the scripts for those addEventListener calls
830
+ - GC pressure: high GC pause counts or duration suggest excessive short-lived object creation — look for hot loops creating objects
831
+ - Layout thrashing: forced synchronous layouts caused by reading layout properties (offsetHeight, getBoundingClientRect) after DOM writes
832
+
833
+ ## Severity classification
834
+
835
+ Assign severity based on measured impact — do NOT guess:
836
+
837
+ - **critical** — A blocking function >500ms, retained heap object >5MB,
838
+ render-blocking resource >200KB, listener addCount > 10× removeCount,
839
+ or GC pauses totalling >500ms
840
+ - **warning** — A blocking function 100–500ms, retained heap object 1–5MB,
841
+ render-blocking resource 50–200KB, listener addCount > 2× removeCount,
842
+ or GC pauses totalling 100–500ms
843
+ - **info** — Blocking function 50–100ms, retained object <1MB, minor
844
+ optimisation opportunities, or observations about dependency usage
845
+
846
+ Always base severity on the actual numbers from the captured data — never inflate.
847
+
848
+ ## Verification rules
849
+
850
+ These rules are mandatory for every finding:
851
+
852
+ 1. **ALWAYS read the source file** in /scripts/, /styles/, or /html/ and
853
+ verify the code BEFORE suggesting a fix. Never suggest a fix for code
854
+ you have not read.
855
+ 2. **Never guess at line numbers** — confirm by reading the file. If the
856
+ trace reports a line number but the source doesn't match, say so.
857
+ 3. Each \`suggestedFix\` MUST reference the actual current code and describe
858
+ what to change. Include a before/after snippet from the real source.
859
+ 4. If the heap summary, trace summary, and runtime summary all show healthy
860
+ numbers, state that the page is well-optimised — do NOT manufacture
861
+ findings.
862
+ 5. Never report a finding based solely on a URL or resource name — always
863
+ read the actual content to confirm the issue.
864
+
865
+ ## Cross-referencing data
866
+
867
+ - When a script appears in BOTH /trace/runtime/blocking-functions.json (CPU)
868
+ AND /heap/summary.json (memory), mention both dimensions in the finding.
869
+ - Check /trace/runtime/event-listeners.json for listener imbalances and
870
+ cross-reference with the actual addEventListener calls in /scripts/.
871
+ - Use /trace/network-waterfall.json to identify sequential chains, then read
872
+ the initiating script to confirm the dependency.
873
+ - When GC pauses are significant, cross-reference with heap data to identify
874
+ which constructors or allocation patterns are responsible.
875
+
876
+ ## Estimating impactMs
877
+
878
+ Every finding should include an \`impactMs\` estimate when possible:
879
+
880
+ - For render-blocking resources: impactMs ≈ the resource's load duration
881
+ (from the network waterfall) that could be deferred.
882
+ - For blocking functions: impactMs ≈ the function's duration minus 50ms
883
+ (the long-task threshold).
884
+ - For memory issues: impactMs may not apply — use \`retainedSize\` instead.
885
+ - If you cannot reasonably estimate the savings, omit impactMs rather than
886
+ guessing.
887
+
888
+ ## Output guidelines
889
+
890
+ - Report 3–7 findings, ordered by impact (mix of memory, page-load, and runtime if all have issues)
891
+ - Be specific — name actual files, functions, object constructors, and retention paths
892
+ - Provide concrete code fixes, not generic advice
893
+ - If heap, trace, and runtime all look healthy, say so — don't manufacture issues
894
+
895
+ ## Structured output fields
896
+
897
+ For each finding, fill in as many fields as applicable:
898
+
899
+ - \`sourceFile\` — the workspace path (e.g. /scripts/app.js) or resource URL
900
+ where the issue occurs. Always set this when you can identify the file.
901
+ - \`lineNumber\` — the 1-based line number in the source file. Only set after
902
+ verifying by reading the file.
903
+ - \`confidence\` — \`high\` if you read the source and confirmed the issue,
904
+ \`medium\` if the profiling data strongly suggests it but you couldn't fully
905
+ verify, \`low\` if inferred from patterns.
906
+ - \`estimatedSavingsMs\` — your estimate of time saved if the fix is applied.
907
+ - \`beforeCode\` — a snippet of the CURRENT problematic code, copied from the
908
+ source file you read. Keep it focused (5–15 lines).
909
+ - \`afterCode\` — the IMPROVED code snippet showing the fix. Must be a drop-in
910
+ replacement for \`beforeCode\`.
911
+ - \`impactMs\` — the current measured cost (e.g. blocking function duration,
912
+ resource load time).`;
913
+
914
+ // src/vitest/prompts.ts
915
+ var VITEST_SYSTEM_PROMPT = `You are an expert in JavaScript/TypeScript performance optimization.
916
+ You have access to a workspace containing V8 CPU profiling data captured during
917
+ a Vitest test run. The workspace may also include V8 heap profiling data
918
+ captured via Node's \`--heap-prof\` (allocation sampling).
919
+
920
+ The profiling data covers BOTH the test code AND the application code being tested.
921
+
922
+ **Your primary goal is to analyze the PERFORMANCE OF THE APPLICATION CODE
923
+ being tested** — the functions, modules, and algorithms that the developer
924
+ wrote and is benchmarking or testing. Test infrastructure overhead (Vitest,
925
+ tinybench, test setup) is secondary context.
926
+
927
+ ## Source categories
928
+
929
+ Every hot function and script in the workspace has a \`sourceCategory\` field:
930
+
931
+ - **application** — Code in the user's project (the code being tested).
932
+ This is your PRIMARY focus. Find bottlenecks, inefficiencies, and
933
+ optimization opportunities in these functions.
934
+ - **dependency** — Third-party code in node_modules. Report when a dependency
935
+ is a significant bottleneck, since the developer may be able to choose
936
+ an alternative, configure it differently, or avoid calling it in hot paths.
937
+ - **test** — Test files. Only mention if the test setup itself is creating
938
+ artificial overhead that masks application performance.
939
+ - **framework** — Vitest/tinybench/V8 internals. Generally ignore unless
940
+ they dominate the profile in an unexpected way.
941
+
942
+ ## Workspace structure
943
+
944
+ - /summary.json — Overall test run: total tests, duration, pass/fail, GC stats
945
+ - /timing/overview.json — Per-file test durations and individual test times
946
+ - /timing/slow-tests.json — Tests exceeding the slow threshold
947
+ - /profiles/index.json — Manifest mapping test files to their CPU profiles
948
+ - /profiles/<file>.json — CPU profile summary: hot functions (with sourceCategory),
949
+ call trees, GC samples, script breakdown (with sourceCategory)
950
+ - /heap-profiles/index.json — (optional) Manifest mapping test files to heap profiles
951
+ - /heap-profiles/<file>.json — (optional) Heap profile summary: allocation hotspots,
952
+ per-script allocated bytes (with sourceCategory)
953
+ - /hot-functions/application.json — **START HERE**: Hot functions from application code only.
954
+ Each entry includes a \`sourceSnippet\` (lines around the hot line) and \`workspacePath\`
955
+ (path to the full source file in the workspace) when source code is available.
956
+ - /hot-functions/dependencies.json — Hot functions from third-party dependencies
957
+ - /hot-functions/global.json — All hot functions across all categories
958
+ - /scripts/application.json — Per-script time breakdown for application code
959
+ - /scripts/dependencies.json — Per-script time breakdown for dependencies
960
+ - /listener-tracking.json — (optional) Event listener tracking data captured from
961
+ worker processes. Contains per-event-type add/remove counts for EventTarget and
962
+ EventEmitter, plus exceedances where listener counts exceeded maxListeners.
963
+ - /src/index.json — Mapping of source files to their hot functions (quick overview
964
+ of which files matter and what bottlenecks they contain)
965
+ - /tests/<relative-path> — Test source files (directory structure preserved)
966
+ - /src/<relative-path> — Application and dependency source files referenced by
967
+ hot functions (directory structure preserved from the project root)
968
+
969
+ ## Your workflow
970
+
971
+ 1. Read /hot-functions/application.json FIRST — these are the application-level
972
+ bottlenecks the developer wants to optimize
973
+ 2. Read /scripts/application.json for the per-file view of application code time
974
+ 3. Read /hot-functions/dependencies.json for costly dependency calls
975
+ 4. If present, read /heap-profiles/index.json and /heap-profiles/<file>.json to identify
976
+ allocation hotspots (functions/scripts allocating lots of bytes)
977
+ 5. If present, read /listener-tracking.json for event listener add/remove patterns
978
+ and listener exceedances (too many listeners on a single target)
979
+ 6. Read /summary.json and /timing/overview.json for the big picture
980
+ 7. Read CPU profiles in /profiles/ for detailed call trees of the slowest tests
981
+ 8. Read the actual source code in /src/ and /tests/ to understand root causes
982
+ 9. Provide specific, actionable fixes targeting the application code
983
+
984
+ ## What to look for
985
+
986
+ ### Application code bottlenecks (PRIMARY FOCUS)
987
+ - Functions with high self time — where is the application spending CPU?
988
+ - Expensive algorithms: O(n²) loops, unnecessary sorting, repeated work
989
+ - String/JSON operations: excessive serialization, string concatenation in loops
990
+ - Object allocation hotspots: functions creating many short-lived objects
991
+ - Synchronous blocking: file I/O, crypto, or compression in hot paths
992
+ - Redundant computation: values computed repeatedly that could be cached/memoized
993
+ - Data structure choices: using arrays where Maps/Sets would be O(1)
994
+
995
+ ### Dependency-related bottlenecks
996
+ - Dependencies consuming disproportionate CPU — suggest alternatives or configuration
997
+ - Unnecessary calls to expensive dependency APIs in hot paths
998
+ - Dependencies pulled in for simple operations that could be hand-written
999
+
1000
+ ### GC pressure from application code
1001
+ - Application functions creating many temporary objects in tight loops
1002
+ - Large array/object allocations that could be pooled or reused
1003
+ - Closures capturing large scopes unnecessarily
1004
+
1005
+ ### Allocation hotspots (from heap profiles, if present)
1006
+ - Functions allocating a large share of total bytes (even if CPU isn't dominant)
1007
+ - Scripts/modules responsible for most allocation — suggest caching, reuse, pooling,
1008
+ or avoiding intermediate arrays/objects
1009
+ - When allocation hotspots match CPU hotspots, prioritize fixes there first
1010
+
1011
+ ### Call chain analysis
1012
+ - Each hot function includes a \`callerChain\` field — the chain of callers
1013
+ from the hot function up toward the entry point. Use this to understand
1014
+ WHY a function is hot and which application-level call triggers it.
1015
+ - Trace expensive call trees to find which APPLICATION function triggers them
1016
+ - Follow the call tree from application entry points down to the hot leaf functions
1017
+ - Identify which application-level design decisions lead to the bottleneck
1018
+
1019
+ ### Event listener tracking (from /listener-tracking.json, if present)
1020
+ - **Exceedances** — when a single EventTarget or EventEmitter accumulates more
1021
+ listeners than its maxListeners threshold (default 10), this is a strong
1022
+ signal of a listener leak. The exceedance data includes the target type
1023
+ (e.g. AbortSignal), event name, listener count, and a stack trace snippet
1024
+ pointing to the code that registered the excess listener.
1025
+ - **Add/remove imbalances** — when \`addCount\` significantly exceeds
1026
+ \`removeCount\` for a given event type, listeners are being registered but
1027
+ not cleaned up. This causes memory growth and eventually GC pressure.
1028
+ Look for patterns like:
1029
+ - AbortSignal abort listeners not cleaned up (common with fetch/streams)
1030
+ - EventEmitter listeners added in loops without corresponding removal
1031
+ - Missing \`{ once: true }\` option or \`AbortController\` cleanup
1032
+ - When exceedances or imbalances are found, read the source code to identify
1033
+ the root cause and suggest specific fixes (e.g. using \`AbortController\`,
1034
+ \`removeEventListener\`, \`{ once: true }\`, or restructuring listener
1035
+ registration to avoid accumulation).
1036
+
1037
+ ### Test infrastructure (SECONDARY — only if impactful)
1038
+ - Test setup creating artificial overhead that dwarfs application execution
1039
+ - Benchmarks measuring setup cost instead of application performance
1040
+ - Only mention if it prevents getting clean application performance data
1041
+
1042
+ ## Finding categories
1043
+
1044
+ Each finding MUST use one of these exact category values:
1045
+
1046
+ - **algorithm** — Inefficient algorithm: O(n²) loops, brute-force search, repeated work
1047
+ - **serialization** — Excessive JSON.stringify/parse, string concatenation, encoding
1048
+ - **allocation** — Excessive object/array creation causing GC pressure
1049
+ - **event-handling** — Listener leaks, unbounded event handler accumulation
1050
+ - **hot-function** — Generic CPU-hot function that doesn't fit a more specific category
1051
+ - **gc-pressure** — High garbage collection overhead
1052
+ - **listener-leak** — Event listeners not cleaned up properly
1053
+ - **unnecessary-computation** — Redundant work that could be cached or eliminated
1054
+ - **blocking-io** — Synchronous I/O or blocking operations in hot paths
1055
+ - **dependency-bottleneck** — Expensive dependency call the developer can optimize
1056
+ - **slow-test** — Test itself is slow due to setup or teardown
1057
+ - **expensive-setup** — Costly test setup (beforeAll/beforeEach)
1058
+ - **import-overhead** — Expensive module imports at test time
1059
+ - **other** — Doesn't fit any of the above
1060
+
1061
+ Prefer more specific categories (algorithm, serialization, allocation, event-handling)
1062
+ over generic ones (hot-function, other) when the root cause is clear.
1063
+
1064
+ ## Severity classification
1065
+
1066
+ Assign severity based on measured impact — do NOT guess:
1067
+
1068
+ - **critical** — A single function consuming >15% self-time, listener exceedances
1069
+ (count exceeding maxListeners), or GC overhead >10% of total profile duration
1070
+ - **warning** — A function consuming 5–15% self-time, listener add/remove imbalance
1071
+ where addCount > 2× removeCount, or GC overhead between 5–10%
1072
+ - **info** — A function consuming <5% self-time, minor inefficiencies,
1073
+ dependency observations, or small optimisation opportunities
1074
+
1075
+ Always base severity on the actual numbers from the profiling data — never inflate.
1076
+
1077
+ ## Verification rules
1078
+
1079
+ These rules are mandatory for every finding:
1080
+
1081
+ 1. **ALWAYS read the source file** in /src/ or /tests/ and verify the code
1082
+ BEFORE suggesting a fix. Never suggest a fix for code you have not read.
1083
+ 2. **Never guess at line numbers** — confirm by reading the file. If the profile
1084
+ reports line 42 but the source at line 42 doesn't match, say so.
1085
+ 3. Each \`suggestedFix\` MUST reference the actual current code and describe
1086
+ what to change. Include a before/after snippet from the real source.
1087
+ 4. If /hot-functions/application.json is empty or every function is <1%
1088
+ self-time, state that the application code is efficient — do NOT
1089
+ manufacture findings.
1090
+ 5. Never report a finding based solely on a function name — always read the
1091
+ implementation to confirm the issue exists.
1092
+
1093
+ ## Cross-referencing data
1094
+
1095
+ - When a function appears in BOTH /hot-functions/ (CPU hotspot) AND
1096
+ /heap-profiles/ (allocation hotspot), prioritise it and mention both
1097
+ dimensions in the finding.
1098
+ - Cross-reference /hot-functions/ data with /heap-profiles/ when both are
1099
+ present to find functions that are expensive in both CPU and memory.
1100
+ - Check whether hot dependency calls originate from application code by
1101
+ tracing the call tree in /profiles/.
1102
+ - When /listener-tracking.json is present, cross-reference exceedance stack
1103
+ traces with the source code in /src/ to pinpoint the registration site.
1104
+ - /metrics/current.json contains pre-computed aggregate metrics (suite totals,
1105
+ CPU category breakdown, top hot functions). Use it for the big-picture
1106
+ numbers when sizing impact.
1107
+
1108
+ ## Estimating impactMs
1109
+
1110
+ Every finding should include an \`impactMs\` estimate when possible:
1147
1111
 
1148
- // src/analysis/parser.ts
1149
- function parseSnapshot(rawSnapshot) {
1150
- const v8 = JSON.parse(rawSnapshot.data);
1151
- const meta = v8.snapshot.meta;
1152
- const nodeFieldCount = meta.node_fields.length;
1153
- const edgeFieldCount = meta.edge_fields.length;
1154
- const nodeTypes = meta.node_types[0];
1155
- const edgeTypes = meta.edge_types[0];
1156
- const nodeTypeIdx = meta.node_fields.indexOf("type");
1157
- const nodeNameIdx = meta.node_fields.indexOf("name");
1158
- const nodeIdIdx = meta.node_fields.indexOf("id");
1159
- const nodeSelfSizeIdx = meta.node_fields.indexOf("self_size");
1160
- const nodeEdgeCountIdx = meta.node_fields.indexOf("edge_count");
1161
- const nodeDetachednessIdx = meta.node_fields.indexOf("detachedness");
1162
- const edgeTypeIdx = meta.edge_fields.indexOf("type");
1163
- const edgeNameIdx = meta.edge_fields.indexOf("name_or_index");
1164
- const edgeToNodeIdx = meta.edge_fields.indexOf("to_node");
1165
- const nodeCount = v8.snapshot.node_count;
1166
- const nodes = [];
1167
- let edgeOffset = 0;
1168
- let totalSize = 0;
1169
- for (let i = 0;i < nodeCount; i++) {
1170
- const base = i * nodeFieldCount;
1171
- const selfSize = v8.nodes[base + nodeSelfSizeIdx] ?? 0;
1172
- totalSize += selfSize;
1173
- nodes.push({
1174
- ordinal: i,
1175
- type: nodeTypes[v8.nodes[base + nodeTypeIdx] ?? 0] ?? "unknown",
1176
- name: v8.strings[v8.nodes[base + nodeNameIdx] ?? 0] ?? "",
1177
- id: v8.nodes[base + nodeIdIdx] ?? 0,
1178
- selfSize,
1179
- edgeCount: v8.nodes[base + nodeEdgeCountIdx] ?? 0,
1180
- detachedness: nodeDetachednessIdx >= 0 ? v8.nodes[base + nodeDetachednessIdx] ?? 0 : 0,
1181
- edgeStartIndex: edgeOffset
1182
- });
1183
- edgeOffset += (v8.nodes[base + nodeEdgeCountIdx] ?? 0) * edgeFieldCount;
1184
- }
1185
- const adjacency = Array.from({ length: nodeCount }, () => []);
1186
- for (let i = 0;i < nodeCount; i++)
1187
- adjacency[i] = [];
1188
- const reverseAdj = Array.from({ length: nodeCount }, () => []);
1189
- for (let i = 0;i < nodeCount; i++)
1190
- reverseAdj[i] = [];
1191
- for (let i = 0;i < nodeCount; i++) {
1192
- const node = nodes[i];
1193
- for (let e = 0;e < node.edgeCount; e++) {
1194
- const edgeBase = node.edgeStartIndex + e * edgeFieldCount;
1195
- const edgeTypeVal = edgeTypes[v8.edges[edgeBase + edgeTypeIdx] ?? 0];
1196
- if (edgeTypeVal === "weak")
1197
- continue;
1198
- const toNodeArrayIdx = v8.edges[edgeBase + edgeToNodeIdx] ?? 0;
1199
- const toOrdinal = toNodeArrayIdx / nodeFieldCount;
1200
- if (toOrdinal >= 0 && toOrdinal < nodeCount) {
1201
- adjacency[i].push(toOrdinal);
1202
- reverseAdj[toOrdinal].push(i);
1203
- }
1204
- }
1205
- }
1206
- const visited = new Uint8Array(nodeCount);
1207
- const bfsOrder = [];
1208
- const queue = [0];
1209
- visited[0] = 1;
1210
- while (queue.length > 0) {
1211
- const curr = queue.shift();
1212
- bfsOrder.push(curr);
1213
- for (const next of adjacency[curr]) {
1214
- if (!visited[next]) {
1215
- visited[next] = 1;
1216
- queue.push(next);
1217
- }
1218
- }
1219
- }
1220
- const dominators = new Int32Array(nodeCount).fill(-1);
1221
- dominators[0] = 0;
1222
- const nodeToRpo = new Int32Array(nodeCount).fill(-1);
1223
- for (let i = 0;i < bfsOrder.length; i++) {
1224
- nodeToRpo[bfsOrder[i]] = i;
1225
- }
1226
- function intersect(b1Init, b2Init) {
1227
- let b1 = b1Init;
1228
- let b2 = b2Init;
1229
- let finger1 = nodeToRpo[b1];
1230
- let finger2 = nodeToRpo[b2];
1231
- while (finger1 !== finger2) {
1232
- while (finger1 > finger2) {
1233
- b1 = dominators[b1];
1234
- if (b1 < 0)
1235
- return 0;
1236
- finger1 = nodeToRpo[b1];
1237
- }
1238
- while (finger2 > finger1) {
1239
- b2 = dominators[b2];
1240
- if (b2 < 0)
1241
- return 0;
1242
- finger2 = nodeToRpo[b2];
1243
- }
1244
- }
1245
- return b1;
1246
- }
1247
- let changed = true;
1248
- while (changed) {
1249
- changed = false;
1250
- for (let i = 1;i < bfsOrder.length; i++) {
1251
- const node = bfsOrder[i];
1252
- let newIdom = -1;
1253
- for (const pred of reverseAdj[node]) {
1254
- if (dominators[pred] < 0)
1255
- continue;
1256
- if (newIdom < 0) {
1257
- newIdom = pred;
1258
- } else {
1259
- newIdom = intersect(newIdom, pred);
1260
- }
1261
- }
1262
- if (newIdom >= 0 && dominators[node] !== newIdom) {
1263
- dominators[node] = newIdom;
1264
- changed = true;
1265
- }
1266
- }
1267
- }
1268
- const retainedSizes = new Float64Array(nodeCount);
1269
- for (let i = 0;i < nodeCount; i++) {
1270
- retainedSizes[i] = nodes[i].selfSize;
1112
+ - Use the hot function's \`selfTime\` as the baseline cost.
1113
+ - Estimate what fraction of that cost the fix would eliminate
1114
+ (e.g. an O(n²) → O(n) fix on data of size 1000 might eliminate ~99%).
1115
+ - impactMs = selfTime × estimated fraction eliminated.
1116
+ - Example: a function with selfTime 200ms in a 1000ms run, where an
1117
+ algorithm fix would remove ~80% of the work → impactMs ≈ 160.
1118
+ - If you cannot reasonably estimate the savings, omit impactMs rather than
1119
+ guessing.
1120
+
1121
+ ## Output guidelines
1122
+
1123
+ - Report 3–7 findings, ordered by impact ON THE APPLICATION CODE
1124
+ - Focus findings on functions the developer CAN change (application code first,
1125
+ then dependency usage patterns, then test structure)
1126
+ - Be specific — name actual files, functions, line numbers from the source code
1127
+ - Provide concrete code-level fixes, not generic advice
1128
+ - When reporting a dependency bottleneck, explain what application code is
1129
+ calling it and how the developer can reduce that cost
1130
+ - If the application code is already efficient, say so — don't force findings
1131
+ about test infrastructure just to fill the report
1132
+
1133
+ ## Structured output fields
1134
+
1135
+ For each finding, fill in as many fields as applicable:
1136
+
1137
+ - \`sourceFile\` — the workspace path (e.g. /src/utils/parser.ts) or original
1138
+ file path where the issue occurs. Always set this when you can identify
1139
+ the file.
1140
+ - \`lineNumber\` the 1-based line number in the source file. Only set after
1141
+ verifying by reading the file.
1142
+ - \`confidence\` — \`high\` if you read the source and confirmed the issue,
1143
+ \`medium\` if the profiling data strongly suggests it but you couldn't fully
1144
+ verify, \`low\` if inferred from patterns.
1145
+ - \`estimatedSavingsMs\` — your estimate of time saved if the fix is applied.
1146
+ - \`beforeCode\` — a snippet of the CURRENT problematic code, copied from the
1147
+ source file you read. Keep it focused (5–15 lines).
1148
+ - \`afterCode\` — the IMPROVED code snippet showing the fix. Must be a drop-in
1149
+ replacement for \`beforeCode\`.
1150
+ - \`affectedTests\` list of test names that exercise this code path and would
1151
+ benefit from the fix.
1152
+ - \`impactMs\` the current measured cost (e.g. selfTime of the hot function).`;
1153
+
1154
+ // src/schema.ts
1155
+ import { z } from "zod";
1156
+ var ALL_CATEGORIES = [
1157
+ "memory-leak",
1158
+ "large-retained-object",
1159
+ "detached-dom",
1160
+ "render-blocking",
1161
+ "long-task",
1162
+ "unused-code",
1163
+ "waterfall-bottleneck",
1164
+ "large-asset",
1165
+ "frame-blocking-function",
1166
+ "listener-leak",
1167
+ "gc-pressure",
1168
+ "slow-test",
1169
+ "expensive-setup",
1170
+ "hot-function",
1171
+ "unnecessary-computation",
1172
+ "import-overhead",
1173
+ "dependency-bottleneck",
1174
+ "algorithm",
1175
+ "serialization",
1176
+ "allocation",
1177
+ "event-handling",
1178
+ "blocking-io",
1179
+ "other"
1180
+ ];
1181
+ var FindingSchema = z.object({
1182
+ severity: z.enum(["critical", "warning", "info"]),
1183
+ title: z.string().describe("Short title for the finding"),
1184
+ description: z.string().describe("Detailed explanation of the issue"),
1185
+ category: z.string().describe(`Category of the performance issue. Use one of: ${ALL_CATEGORIES.join(", ")}`),
1186
+ resourceUrl: z.string().optional().describe("URL of the resource involved"),
1187
+ workspacePath: z.string().optional().describe("Path in the VFS workspace"),
1188
+ impactMs: z.number().optional().describe("Estimated current cost in ms (e.g. selfTime of the hot function)"),
1189
+ estimatedSavingsMs: z.number().optional().describe("Estimated time savings in ms if the fix is applied. Computed as impactMs × fraction eliminated by the fix."),
1190
+ confidence: z.enum(["high", "medium", "low"]).optional().describe("How confident you are in this finding. high = verified in source code, medium = strong signal but partial verification, low = inferred from data patterns"),
1191
+ retainedSize: z.number().optional().describe("Retained heap size in bytes"),
1192
+ retainerPath: z.array(z.string()).optional().describe("Object retention path in the heap"),
1193
+ sourceFile: z.string().optional().describe("Primary source file where the issue occurs (workspace path or original path)"),
1194
+ lineNumber: z.number().optional().describe("Line number in the source file where the issue occurs (1-based)"),
1195
+ suggestedFix: z.string().describe("Code snippet or guidance to fix the issue"),
1196
+ beforeCode: z.string().optional().describe("The current problematic code snippet from the source file"),
1197
+ afterCode: z.string().optional().describe("The improved code snippet that fixes the issue"),
1198
+ testFile: z.string().optional().describe("Test file path (for test performance findings)"),
1199
+ affectedTests: z.array(z.string()).optional().describe("Test names that would benefit from this fix (for test performance findings)"),
1200
+ hotFunction: z.object({
1201
+ name: z.string(),
1202
+ scriptUrl: z.string(),
1203
+ lineNumber: z.number(),
1204
+ selfTime: z.number(),
1205
+ selfPercent: z.number()
1206
+ }).optional().describe("Hot function details (for hot-function findings)")
1207
+ });
1208
+ var FindingsSchema = z.object({
1209
+ findings: z.array(FindingSchema)
1210
+ });
1211
+
1212
+ // src/output/progress.ts
1213
+ class TodoProgressRenderer {
1214
+ spinner;
1215
+ lastStatusByKey = new Map;
1216
+ lastInProgressKey;
1217
+ baseSpinnerText;
1218
+ printedHeader = false;
1219
+ constructor(spinner) {
1220
+ this.spinner = spinner;
1221
+ this.baseSpinnerText = spinner.text;
1271
1222
  }
1272
- for (let i = bfsOrder.length - 1;i > 0; i--) {
1273
- const node = bfsOrder[i];
1274
- const dom = dominators[node];
1275
- if (dom >= 0 && dom !== node) {
1276
- retainedSizes[dom] = retainedSizes[dom] + retainedSizes[node];
1277
- }
1223
+ printHeaderOnce() {
1224
+ if (this.printedHeader)
1225
+ return;
1226
+ this.printedHeader = true;
1227
+ const header = "Performance analysis progress:";
1228
+ this.spinner.stopAndPersist({ symbol: " ", text: header });
1229
+ this.spinner.start();
1278
1230
  }
1279
- const indexed = bfsOrder.filter((i) => i > 0).map((i) => ({ ordinal: i, retainedSize: retainedSizes[i] })).sort((a, b) => b.retainedSize - a.retainedSize).slice(0, 50);
1280
- function getEdgeName(toOrdinal) {
1281
- const dom = dominators[toOrdinal];
1282
- if (dom < 0)
1283
- return "";
1284
- const domNode = nodes[dom];
1285
- for (let e = 0;e < domNode.edgeCount; e++) {
1286
- const edgeBase = domNode.edgeStartIndex + e * edgeFieldCount;
1287
- const toNodeArrayIdx = v8.edges[edgeBase + edgeToNodeIdx] ?? 0;
1288
- if (toNodeArrayIdx / nodeFieldCount === toOrdinal) {
1289
- const nameOrIndex = v8.edges[edgeBase + edgeNameIdx] ?? 0;
1290
- const edgeTypeVal = edgeTypes[v8.edges[edgeBase + edgeTypeIdx] ?? 0];
1291
- if (edgeTypeVal === "element")
1292
- return `[${nameOrIndex}]`;
1293
- return v8.strings[nameOrIndex] ?? String(nameOrIndex);
1231
+ handleChunk(chunk) {
1232
+ const todos = extractTodosFromStreamChunk(chunk);
1233
+ if (!todos)
1234
+ return;
1235
+ for (const todo of todos) {
1236
+ const key = todo.id && String(todo.id) || todo.content;
1237
+ const prevStatus = this.lastStatusByKey.get(key);
1238
+ const nextStatus = todo.status;
1239
+ if (prevStatus !== nextStatus) {
1240
+ this.lastStatusByKey.set(key, nextStatus);
1241
+ if (nextStatus === "completed" && prevStatus !== "completed") {
1242
+ this.printHeaderOnce();
1243
+ this.spinner.stopAndPersist({ symbol: " ", text: ` ✓ ${todo.content}` });
1244
+ this.spinner.start();
1245
+ }
1246
+ if (nextStatus === "in_progress" && this.lastInProgressKey !== key) {
1247
+ this.lastInProgressKey = key;
1248
+ this.printHeaderOnce();
1249
+ const base = this.baseSpinnerText ?? this.spinner.text;
1250
+ this.spinner.text = base ? `${base} (${todo.content})` : todo.content;
1251
+ }
1294
1252
  }
1295
1253
  }
1296
- return "";
1297
- }
1298
- function getRetainerPath(targetOrdinal, maxDepth = 10) {
1299
- const path = [];
1300
- let current = targetOrdinal;
1301
- const pathVisited = new Set;
1302
- for (let depth = 0;depth < maxDepth; depth++) {
1303
- const node = nodes[current];
1304
- const edgeName = getEdgeName(current);
1305
- path.unshift(edgeName ? `${node.name || node.type}` : node.name || node.type);
1306
- if (current === 0)
1307
- break;
1308
- pathVisited.add(current);
1309
- const dom = dominators[current];
1310
- if (dom < 0 || dom === current || pathVisited.has(dom))
1311
- break;
1312
- current = dom;
1313
- }
1314
- return path;
1315
1254
  }
1316
- const largestObjects = indexed.map(({ ordinal, retainedSize }) => {
1317
- const node = nodes[ordinal];
1318
- return {
1319
- name: node.name || `(${node.type})`,
1320
- type: node.type,
1321
- selfSize: node.selfSize,
1322
- retainedSize,
1323
- retainerPath: getRetainerPath(ordinal)
1324
- };
1325
- });
1326
- const typeMap = new Map;
1327
- for (const node of nodes) {
1328
- if (!visited[node.ordinal])
1255
+ }
1256
+ function extractTodosFromStreamChunk(chunk) {
1257
+ if (!chunk || typeof chunk !== "object")
1258
+ return;
1259
+ const direct = chunk;
1260
+ if (Array.isArray(direct.todos))
1261
+ return direct.todos;
1262
+ for (const value of Object.values(chunk)) {
1263
+ if (!value || typeof value !== "object")
1329
1264
  continue;
1330
- const existing = typeMap.get(node.type);
1331
- if (existing) {
1332
- existing.count++;
1333
- existing.totalSize += node.selfSize;
1334
- } else {
1335
- typeMap.set(node.type, { count: 1, totalSize: node.selfSize });
1336
- }
1265
+ const nested = value;
1266
+ if (Array.isArray(nested.todos))
1267
+ return nested.todos;
1337
1268
  }
1338
- const typeStats = Array.from(typeMap.entries()).map(([type, stats]) => ({
1339
- type,
1340
- count: stats.count,
1341
- totalSize: stats.totalSize,
1342
- avgSize: stats.count > 0 ? Math.round(stats.totalSize / stats.count) : 0
1343
- })).sort((a, b) => b.totalSize - a.totalSize);
1344
- const ctorMap = new Map;
1345
- for (const node of nodes) {
1346
- if (!visited[node.ordinal])
1347
- continue;
1348
- if (node.type !== "object" || !node.name)
1269
+ }
1270
+
1271
+ // src/analysis/agent.ts
1272
+ async function invokeWithTodoStreaming(agent, userMessage, spinner) {
1273
+ const renderer = new TodoProgressRenderer(spinner);
1274
+ const stream = await agent.stream({ messages: [{ role: "user", content: userMessage }] }, { streamMode: ["updates", "values"] });
1275
+ let lastValues;
1276
+ for await (const item of stream) {
1277
+ if (Array.isArray(item) && item.length === 2) {
1278
+ const mode = item[0];
1279
+ const chunk = item[1];
1280
+ renderer.handleChunk(chunk);
1281
+ if (mode === "values")
1282
+ lastValues = chunk;
1349
1283
  continue;
1350
- const existing = ctorMap.get(node.name);
1351
- if (existing) {
1352
- existing.count++;
1353
- existing.totalSize += node.selfSize;
1354
- } else {
1355
- ctorMap.set(node.name, { count: 1, totalSize: node.selfSize });
1356
1284
  }
1285
+ renderer.handleChunk(item);
1286
+ lastValues = item;
1357
1287
  }
1358
- const constructorStats = Array.from(ctorMap.entries()).map(([ctor, stats]) => ({
1359
- constructor: ctor,
1360
- count: stats.count,
1361
- totalSize: stats.totalSize,
1362
- avgSize: stats.count > 0 ? Math.round(stats.totalSize / stats.count) : 0
1363
- })).sort((a, b) => b.totalSize - a.totalSize).slice(0, 30);
1364
- const detachedExamples = [];
1365
- let detachedCount = 0;
1366
- let detachedTotalSize = 0;
1367
- for (const node of nodes) {
1368
- if (!visited[node.ordinal])
1369
- continue;
1370
- const isDetached = nodeDetachednessIdx >= 0 && node.detachedness > 0 || node.name.includes("Detached") || node.type === "native" && /HTML\w*Element|Document|Node/.test(node.name) && node.name.includes("Detached");
1371
- if (isDetached) {
1372
- detachedCount++;
1373
- detachedTotalSize += node.selfSize;
1374
- if (detachedExamples.length < 10) {
1375
- detachedExamples.push({
1376
- name: node.name || `(${node.type})`,
1377
- retainerPath: getRetainerPath(node.ordinal)
1378
- });
1379
- }
1380
- }
1288
+ return lastValues;
1289
+ }
1290
+ function buildPageLoadUserMessage(ctx) {
1291
+ const { url, heapSummary, traceResult } = ctx;
1292
+ const m = traceResult.metrics;
1293
+ const reqCount = traceResult.networkRequests.length;
1294
+ const renderBlocking = traceResult.networkRequests.filter((r) => r.isRenderBlocking).length;
1295
+ const totalTransfer = traceResult.networkRequests.reduce((s, r) => s + r.encodedSize, 0);
1296
+ const hasRuntime = !!traceResult.runtimeTrace;
1297
+ const lines = [
1298
+ "Analyze the frontend performance data in this workspace.",
1299
+ "",
1300
+ `URL: ${url}`,
1301
+ `Page load: ${Math.round(m.loadComplete)}ms | FCP: ${Math.round(m.firstContentfulPaint)}ms | LCP: ${Math.round(m.largestContentfulPaint)}ms | TBT: ${Math.round(m.totalBlockingTime)}ms`,
1302
+ `Heap: ${formatBytes(heapSummary.metadata.totalSize)} total, ${heapSummary.metadata.nodeCount.toLocaleString()} nodes, ${heapSummary.detachedNodes.count} detached DOM nodes`,
1303
+ `Network: ${reqCount} requests, ${formatBytes(totalTransfer)} transferred, ${renderBlocking} render-blocking`,
1304
+ `Long tasks: ${m.longTasks.length}`
1305
+ ];
1306
+ if (hasRuntime) {
1307
+ const rt = traceResult.runtimeTrace;
1308
+ lines.push(`Runtime trace: ${rt.blockingFunctions.length} blocking functions, ${rt.gcEvents.length} GC events (${Math.round(rt.gcEvents.reduce((s, e) => s + e.duration, 0))}ms total)`);
1381
1309
  }
1382
- const detachedNodes = {
1383
- count: detachedCount,
1384
- totalSize: detachedTotalSize,
1385
- examples: detachedExamples
1386
- };
1387
- const closureNodes = nodes.filter((n) => visited[n.ordinal] && n.type === "closure");
1388
- const closureTotalSize = closureNodes.reduce((sum, n) => sum + n.selfSize, 0);
1389
- const topClosures = closureNodes.sort((a, b) => b.selfSize - a.selfSize).slice(0, 20).map((n) => ({
1390
- name: n.name || "(anonymous)",
1391
- contextSize: n.selfSize,
1392
- retainerPath: getRetainerPath(n.ordinal)
1393
- }));
1394
- const closureStats = {
1395
- count: closureNodes.length,
1396
- totalSize: closureTotalSize,
1397
- topClosures
1398
- };
1399
- return {
1400
- metadata: {
1401
- url: rawSnapshot.url,
1402
- capturedAt: rawSnapshot.capturedAt,
1403
- totalSize,
1404
- nodeCount: v8.snapshot.node_count,
1405
- edgeCount: v8.snapshot.edge_count
1406
- },
1407
- largestObjects,
1408
- typeStats,
1409
- constructorStats,
1410
- detachedNodes,
1411
- closureStats
1412
- };
1310
+ lines.push("", "Available data:", "- /heap/summary.json — parsed heap snapshot", "- /trace/summary.json — page load metrics", "- /trace/network-waterfall.json — request timing and sizes", "- /trace/asset-manifest.json — index of stored assets");
1311
+ if (hasRuntime) {
1312
+ lines.push("- /trace/runtime/summary.json — runtime trace overview", "- /trace/runtime/blocking-functions.json — main thread blocking functions", "- /trace/runtime/event-listeners.json — listener add/remove counts", "- /trace/runtime/frame-breakdown.json — scripting vs layout vs paint vs GC", "- /trace/runtime/raw-events.json — full Chrome trace events");
1313
+ }
1314
+ lines.push("- /scripts/ — JavaScript source files", "- /styles/ — CSS source files", "- /html/document.html — page markup", "", "Explore the workspace, read source files to verify root causes, and provide code-level fixes.");
1315
+ return lines.join(`
1316
+ `);
1317
+ }
1318
+ async function analyze(model, backend, spinner, context) {
1319
+ const agent = createDeepAgent({
1320
+ model,
1321
+ systemPrompt: SYSTEM_PROMPT,
1322
+ backend,
1323
+ responseFormat: providerStrategy(FindingsSchema)
1324
+ });
1325
+ const userMessage = context ? buildPageLoadUserMessage(context) : [
1326
+ "Analyze the frontend performance data in this workspace.",
1327
+ "",
1328
+ "Start by reading /heap/summary.json, /trace/summary.json, and /trace/runtime/summary.json",
1329
+ "to understand the overall picture, then explore source files to verify root causes."
1330
+ ].join(`
1331
+ `);
1332
+ const result = await invokeWithTodoStreaming(agent, userMessage, spinner);
1333
+ const findings = result.structuredResponse.findings;
1334
+ if (!Array.isArray(findings)) {
1335
+ throw new Error("Agent did not return structured findings.");
1336
+ }
1337
+ return findings;
1338
+ }
1339
+ function buildVitestUserMessage(ctx) {
1340
+ const { metrics, hasHeapProfiles, hasListenerTracking } = ctx;
1341
+ const lines = [
1342
+ "Analyze the performance of the APPLICATION CODE being tested in this Vitest workspace.",
1343
+ "",
1344
+ `Test suite: ${metrics.suite.totalTests} tests, total duration ${metrics.suite.totalDuration}ms`,
1345
+ `CPU breakdown: application ${metrics.cpu.applicationPercent}%, dependencies ${metrics.cpu.dependencyPercent}%, GC ${metrics.cpu.gcPercentage}%, idle ${metrics.cpu.idlePercentage}%`,
1346
+ `Slowest file: ${metrics.suite.slowestFile} (${metrics.suite.slowestFileDuration}ms)`,
1347
+ `Slowest test: ${metrics.suite.slowestTestName} (${metrics.suite.slowestTestDuration}ms)`,
1348
+ "",
1349
+ "Available data:",
1350
+ "- /hot-functions/application.json — application-level CPU hotspots",
1351
+ "- /scripts/application.json — per-file CPU time for application code",
1352
+ "- /hot-functions/dependencies.json — dependency CPU hotspots",
1353
+ "- /scripts/dependencies.json — per-file CPU time for dependencies"
1354
+ ];
1355
+ if (hasListenerTracking) {
1356
+ lines.push("- /listener-tracking.json — event listener add/remove patterns and exceedances");
1357
+ }
1358
+ if (hasHeapProfiles) {
1359
+ lines.push("- /heap-profiles/index.json — heap profile manifest", "- /heap-profiles/<file>.json — per-file allocation hotspots");
1360
+ }
1361
+ lines.push("- /summary.json — overall test run summary", "- /timing/overview.json — per-file test durations", "- /timing/slow-tests.json — tests exceeding the slow threshold", "- /profiles/ — full CPU profile summaries with call trees", "- /metrics/current.json — pre-computed aggregate metrics", "- /src/ and /tests/ — source files", "", "Focus findings on the APPLICATION code — what can the developer change in their own", "codebase to improve performance? Read source files to verify root causes before suggesting fixes.");
1362
+ return lines.join(`
1363
+ `);
1364
+ }
1365
+ async function analyzeTestPerformance(model, backend, spinner, context) {
1366
+ const agent = createDeepAgent({
1367
+ model,
1368
+ systemPrompt: VITEST_SYSTEM_PROMPT,
1369
+ backend,
1370
+ responseFormat: providerStrategy(FindingsSchema)
1371
+ });
1372
+ const userMessage = context ? buildVitestUserMessage(context) : [
1373
+ "Analyze the performance of the APPLICATION CODE being tested in this Vitest workspace.",
1374
+ "",
1375
+ "Start with /hot-functions/application.json, then explore source files to verify",
1376
+ "root causes and provide code-level fixes."
1377
+ ].join(`
1378
+ `);
1379
+ const result = await invokeWithTodoStreaming(agent, userMessage, spinner);
1380
+ const findings = result.structuredResponse?.findings;
1381
+ if (!Array.isArray(findings)) {
1382
+ throw new Error(`Failed to analyze test performance: ${result.messages.at(-1)?.text}`);
1383
+ }
1384
+ return findings;
1385
+ }
1386
+ function formatBytes(bytes) {
1387
+ if (bytes < 1024)
1388
+ return `${bytes} B`;
1389
+ if (bytes < 1024 * 1024)
1390
+ return `${(bytes / 1024).toFixed(1)} KB`;
1391
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1413
1392
  }
1414
-
1415
- // src/cli.ts
1416
- init_agent();
1417
1393
 
1418
1394
  // src/sandbox/workspace.ts
1419
1395
  import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
@@ -1578,12 +1554,13 @@ function getListenerImbalances(tracking) {
1578
1554
  }))
1579
1555
  ].filter((c) => c.addCount > c.removeCount + LISTENER_IMBALANCE_THRESHOLD).sort((a, b) => b.addCount - b.removeCount - (a.addCount - a.removeCount));
1580
1556
  }
1557
+ var LISTENER_TRACKING_JSONL = "listener-tracking.jsonl";
1581
1558
  function generateListenerTrackerScript(outputDir) {
1582
1559
  return `// zeitzeuge: Event listener tracker (auto-injected via --import)
1583
1560
  // Tracks EventTarget/EventEmitter listener add/remove patterns
1584
- // and writes a summary to the profile directory on process exit.
1561
+ // and appends a summary line to a shared JSONL file on process exit.
1585
1562
 
1586
- import { writeFileSync } from 'node:fs';
1563
+ import { appendFileSync } from 'node:fs';
1587
1564
  import { join } from 'node:path';
1588
1565
  import EventEmitter from 'node:events';
1589
1566
 
@@ -1717,8 +1694,10 @@ process.on('exit', () => {
1717
1694
  Object.keys(data.emitterCounts).length > 0;
1718
1695
  if (!hasData) return;
1719
1696
 
1720
- const outPath = join(OUTPUT_DIR, 'listener-tracking-' + process.pid + '.json');
1721
- writeFileSync(outPath, JSON.stringify(data));
1697
+ // Append a single JSON line to the shared JSONL file.
1698
+ // Each line is well under PIPE_BUF so O_APPEND guarantees atomicity.
1699
+ const outPath = join(OUTPUT_DIR, '${LISTENER_TRACKING_JSONL}');
1700
+ appendFileSync(outPath, JSON.stringify(data) + '\\n');
1722
1701
  } catch {}
1723
1702
  });
1724
1703
  `;