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 +1006 -1027
- package/dist/output/report.d.ts.map +1 -1
- package/dist/vitest/index.js +1640 -1623
- package/dist/vitest/listener-tracker.d.ts +7 -0
- package/dist/vitest/listener-tracker.d.ts.map +1 -1
- package/dist/vitest/reporter.d.ts +6 -3
- package/dist/vitest/reporter.d.ts.map +1 -1
- package/dist/vitest/workspace.d.ts.map +1 -1
- package/package.json +1 -1
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/
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
|
1331
|
-
if (
|
|
1332
|
-
|
|
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
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
for (const
|
|
1346
|
-
if (
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
|
1561
|
+
// and appends a summary line to a shared JSONL file on process exit.
|
|
1585
1562
|
|
|
1586
|
-
import {
|
|
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
|
-
|
|
1721
|
-
|
|
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
|
`;
|