zeitzeuge 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/dist/analysis/agent.d.ts +19 -0
  4. package/dist/analysis/agent.d.ts.map +1 -0
  5. package/dist/analysis/parser.d.ts +3 -0
  6. package/dist/analysis/parser.d.ts.map +1 -0
  7. package/dist/analysis/prompts.d.ts +2 -0
  8. package/dist/analysis/prompts.d.ts.map +1 -0
  9. package/dist/browser/capture.d.ts +14 -0
  10. package/dist/browser/capture.d.ts.map +1 -0
  11. package/dist/browser/launch.d.ts +6 -0
  12. package/dist/browser/launch.d.ts.map +1 -0
  13. package/dist/browser/runtime-trace.d.ts +52 -0
  14. package/dist/browser/runtime-trace.d.ts.map +1 -0
  15. package/dist/browser/trace.d.ts +8 -0
  16. package/dist/browser/trace.d.ts.map +1 -0
  17. package/dist/cli.d.ts +3 -0
  18. package/dist/cli.d.ts.map +1 -0
  19. package/dist/cli.js +1695 -0
  20. package/dist/models/init.d.ts +3 -0
  21. package/dist/models/init.d.ts.map +1 -0
  22. package/dist/output/report.d.ts +38 -0
  23. package/dist/output/report.d.ts.map +1 -0
  24. package/dist/output/terminal.d.ts +31 -0
  25. package/dist/output/terminal.d.ts.map +1 -0
  26. package/dist/sandbox/workspace.d.ts +33 -0
  27. package/dist/sandbox/workspace.d.ts.map +1 -0
  28. package/dist/schema.d.ts +64 -0
  29. package/dist/schema.d.ts.map +1 -0
  30. package/dist/types.d.ts +245 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/vitest/classify.d.ts +19 -0
  33. package/dist/vitest/classify.d.ts.map +1 -0
  34. package/dist/vitest/heap-profile-parser.d.ts +12 -0
  35. package/dist/vitest/heap-profile-parser.d.ts.map +1 -0
  36. package/dist/vitest/index.d.ts +17 -0
  37. package/dist/vitest/index.d.ts.map +1 -0
  38. package/dist/vitest/index.js +1616 -0
  39. package/dist/vitest/plugin.d.ts +17 -0
  40. package/dist/vitest/plugin.d.ts.map +1 -0
  41. package/dist/vitest/profile-parser.d.ts +13 -0
  42. package/dist/vitest/profile-parser.d.ts.map +1 -0
  43. package/dist/vitest/prompts.d.ts +10 -0
  44. package/dist/vitest/prompts.d.ts.map +1 -0
  45. package/dist/vitest/reporter.d.ts +79 -0
  46. package/dist/vitest/reporter.d.ts.map +1 -0
  47. package/dist/vitest/types.d.ts +231 -0
  48. package/dist/vitest/types.d.ts.map +1 -0
  49. package/dist/vitest/workspace.d.ts +25 -0
  50. package/dist/vitest/workspace.d.ts.map +1 -0
  51. package/package.json +76 -0
@@ -0,0 +1,1616 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/vitest/plugin.ts
5
+ import { resolve as resolve3 } from "node:path";
6
+ import { mkdirSync as mkdirSync2 } from "node:fs";
7
+
8
+ // src/vitest/reporter.ts
9
+ import { readdirSync, readFileSync, existsSync, rmSync, statSync } from "node:fs";
10
+ import { join as join2, resolve as resolve2 } from "node:path";
11
+ import chalk2 from "chalk";
12
+ import ora2 from "ora";
13
+
14
+ // src/vitest/profile-parser.ts
15
+ var MAX_HOT_FUNCTIONS = 50;
16
+ var MAX_CALL_TREES = 10;
17
+ var CALL_TREE_PRUNE_THRESHOLD = 0.01;
18
+ function parseCpuProfile(profile, profilePath) {
19
+ if (!profile.samples || profile.samples.length === 0) {
20
+ return emptySummary(profilePath);
21
+ }
22
+ const totalDurationUs = profile.endTime - profile.startTime;
23
+ const totalDurationMs = totalDurationUs / 1000;
24
+ const statsMap = buildNodeStats(profile);
25
+ computeSelfTime(profile, statsMap);
26
+ computeTotalTime(profile, statsMap);
27
+ const hotFunctions = extractHotFunctions(statsMap, totalDurationUs);
28
+ const expensiveCallTrees = extractCallTrees(profile, statsMap, totalDurationUs);
29
+ const { gcSamples, gcTimeUs, idleTimeUs } = computeSpecialCategories(profile, statsMap);
30
+ const scriptBreakdown = buildScriptBreakdown(statsMap, totalDurationUs);
31
+ return {
32
+ profilePath,
33
+ duration: round(totalDurationMs),
34
+ sampleCount: profile.samples.length,
35
+ hotFunctions,
36
+ expensiveCallTrees,
37
+ gcSamples,
38
+ gcPercentage: totalDurationUs > 0 ? round(gcTimeUs / totalDurationUs * 100) : 0,
39
+ idlePercentage: totalDurationUs > 0 ? round(idleTimeUs / totalDurationUs * 100) : 0,
40
+ scriptBreakdown
41
+ };
42
+ }
43
+ function buildNodeStats(profile) {
44
+ const statsMap = new Map;
45
+ for (const node of profile.nodes) {
46
+ statsMap.set(node.id, {
47
+ node,
48
+ selfTime: 0,
49
+ totalTime: 0,
50
+ parentIds: new Set
51
+ });
52
+ }
53
+ for (const node of profile.nodes) {
54
+ if (node.children) {
55
+ for (const childId of node.children) {
56
+ const childStats = statsMap.get(childId);
57
+ if (childStats) {
58
+ childStats.parentIds.add(node.id);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return statsMap;
64
+ }
65
+ function computeSelfTime(profile, statsMap) {
66
+ const { samples, timeDeltas } = profile;
67
+ for (let i = 0;i < samples.length; i++) {
68
+ const nodeId = samples[i];
69
+ const delta = timeDeltas[i] ?? 0;
70
+ const stats = statsMap.get(nodeId);
71
+ if (stats) {
72
+ stats.selfTime += Math.max(0, delta);
73
+ }
74
+ }
75
+ }
76
+ function computeTotalTime(profile, statsMap) {
77
+ const childCount = new Map;
78
+ const queue = [];
79
+ for (const node of profile.nodes) {
80
+ const numChildren = node.children?.length ?? 0;
81
+ childCount.set(node.id, numChildren);
82
+ if (numChildren === 0) {
83
+ queue.push(node.id);
84
+ }
85
+ }
86
+ while (queue.length > 0) {
87
+ const nodeId = queue.shift();
88
+ const stats = statsMap.get(nodeId);
89
+ if (!stats)
90
+ continue;
91
+ stats.totalTime = stats.selfTime;
92
+ const children = stats.node.children ?? [];
93
+ for (const childId of children) {
94
+ const childStats = statsMap.get(childId);
95
+ if (childStats) {
96
+ stats.totalTime += childStats.totalTime;
97
+ }
98
+ }
99
+ for (const parentId of stats.parentIds) {
100
+ const remaining = (childCount.get(parentId) ?? 1) - 1;
101
+ childCount.set(parentId, remaining);
102
+ if (remaining <= 0) {
103
+ queue.push(parentId);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ function extractHotFunctions(statsMap, totalDurationUs) {
109
+ const results = [];
110
+ for (const stats of statsMap.values()) {
111
+ if (stats.selfTime <= 0)
112
+ continue;
113
+ const fn = stats.node.callFrame.functionName;
114
+ if (fn === "(root)" || fn === "(idle)" || fn === "(program)")
115
+ continue;
116
+ results.push({
117
+ functionName: stats.node.callFrame.functionName || "(anonymous)",
118
+ scriptUrl: stats.node.callFrame.url,
119
+ lineNumber: stats.node.callFrame.lineNumber,
120
+ columnNumber: stats.node.callFrame.columnNumber,
121
+ selfTime: round(stats.selfTime / 1000),
122
+ totalTime: round(stats.totalTime / 1000),
123
+ hitCount: stats.node.hitCount,
124
+ selfPercent: totalDurationUs > 0 ? round(stats.selfTime / totalDurationUs * 100) : 0
125
+ });
126
+ }
127
+ results.sort((a, b) => b.selfTime - a.selfTime);
128
+ return results.slice(0, MAX_HOT_FUNCTIONS);
129
+ }
130
+ function extractCallTrees(profile, statsMap, totalDurationUs) {
131
+ const rootIds = [];
132
+ for (const stats of statsMap.values()) {
133
+ if (stats.parentIds.size === 0) {
134
+ rootIds.push(stats.node.id);
135
+ }
136
+ }
137
+ const trees = [];
138
+ for (const rootId of rootIds) {
139
+ const tree = buildCallTreeNode(rootId, statsMap, totalDurationUs);
140
+ if (tree && tree.totalTime > 0) {
141
+ trees.push(tree);
142
+ }
143
+ }
144
+ trees.sort((a, b) => b.totalTime - a.totalTime);
145
+ return trees.slice(0, MAX_CALL_TREES);
146
+ }
147
+ function buildCallTreeNode(nodeId, statsMap, totalDurationUs) {
148
+ const stats = statsMap.get(nodeId);
149
+ if (!stats)
150
+ return null;
151
+ const totalPercent = totalDurationUs > 0 ? stats.totalTime / totalDurationUs : 0;
152
+ if (totalPercent < CALL_TREE_PRUNE_THRESHOLD)
153
+ return null;
154
+ const children = [];
155
+ for (const childId of stats.node.children ?? []) {
156
+ const childTree = buildCallTreeNode(childId, statsMap, totalDurationUs);
157
+ if (childTree) {
158
+ children.push(childTree);
159
+ }
160
+ }
161
+ children.sort((a, b) => b.totalTime - a.totalTime);
162
+ return {
163
+ functionName: stats.node.callFrame.functionName || "(anonymous)",
164
+ scriptUrl: stats.node.callFrame.url,
165
+ lineNumber: stats.node.callFrame.lineNumber,
166
+ totalTime: round(stats.totalTime / 1000),
167
+ totalPercent: round(totalPercent * 100),
168
+ children
169
+ };
170
+ }
171
+ function computeSpecialCategories(profile, statsMap) {
172
+ let gcSamples = 0;
173
+ let gcTimeUs = 0;
174
+ let idleTimeUs = 0;
175
+ for (const stats of statsMap.values()) {
176
+ const fn = stats.node.callFrame.functionName;
177
+ if (fn.includes("(garbage collector)") || fn === "(GC)") {
178
+ gcSamples += stats.node.hitCount;
179
+ gcTimeUs += stats.selfTime;
180
+ }
181
+ if (fn === "(idle)") {
182
+ idleTimeUs += stats.selfTime;
183
+ }
184
+ }
185
+ return { gcSamples, gcTimeUs, idleTimeUs };
186
+ }
187
+ function buildScriptBreakdown(statsMap, totalDurationUs) {
188
+ const scriptMap = new Map;
189
+ for (const stats of statsMap.values()) {
190
+ const url = stats.node.callFrame.url;
191
+ if (!url)
192
+ continue;
193
+ let entry = scriptMap.get(url);
194
+ if (!entry) {
195
+ entry = { selfTime: 0, functions: new Set };
196
+ scriptMap.set(url, entry);
197
+ }
198
+ entry.selfTime += stats.selfTime;
199
+ if (stats.selfTime > 0) {
200
+ entry.functions.add(`${stats.node.callFrame.functionName}:${stats.node.callFrame.lineNumber}`);
201
+ }
202
+ }
203
+ const results = [];
204
+ for (const [scriptUrl, data] of scriptMap) {
205
+ results.push({
206
+ scriptUrl,
207
+ selfTime: round(data.selfTime / 1000),
208
+ selfPercent: totalDurationUs > 0 ? round(data.selfTime / totalDurationUs * 100) : 0,
209
+ functionCount: data.functions.size
210
+ });
211
+ }
212
+ results.sort((a, b) => b.selfTime - a.selfTime);
213
+ return results;
214
+ }
215
+ function round(n) {
216
+ return Math.round(n * 100) / 100;
217
+ }
218
+ function emptySummary(profilePath) {
219
+ return {
220
+ profilePath,
221
+ duration: 0,
222
+ sampleCount: 0,
223
+ hotFunctions: [],
224
+ expensiveCallTrees: [],
225
+ gcSamples: 0,
226
+ gcPercentage: 0,
227
+ idlePercentage: 0,
228
+ scriptBreakdown: []
229
+ };
230
+ }
231
+
232
+ // src/vitest/heap-profile-parser.ts
233
+ function parseHeapProfile(raw, profilePath) {
234
+ const nodeById = new Map;
235
+ const parentById = new Map;
236
+ const walk = (node, parent) => {
237
+ nodeById.set(node.id, node);
238
+ parentById.set(node.id, parent ? parent.id : null);
239
+ for (const child of node.children ?? [])
240
+ walk(child, node);
241
+ };
242
+ walk(raw.head, null);
243
+ const selfBytesByNode = new Map;
244
+ let totalAllocatedBytes = 0;
245
+ for (const s of raw.samples ?? []) {
246
+ const size = typeof s.size === "number" ? s.size : 0;
247
+ if (!Number.isFinite(size) || size <= 0)
248
+ continue;
249
+ totalAllocatedBytes += size;
250
+ selfBytesByNode.set(s.nodeId, (selfBytesByNode.get(s.nodeId) ?? 0) + size);
251
+ }
252
+ const hotspots = [];
253
+ const scriptMap = new Map;
254
+ for (const [nodeId, selfBytes] of selfBytesByNode.entries()) {
255
+ const node = nodeById.get(nodeId);
256
+ if (!node)
257
+ continue;
258
+ const cf = node.callFrame;
259
+ const scriptUrl = cf?.url ?? "";
260
+ const fnName = cf?.functionName ?? "(anonymous)";
261
+ hotspots.push({
262
+ functionName: fnName,
263
+ scriptUrl,
264
+ lineNumber: cf?.lineNumber ?? -1,
265
+ columnNumber: cf?.columnNumber ?? -1,
266
+ selfBytes,
267
+ selfPercent: 0
268
+ });
269
+ const key = scriptUrl || "(unknown)";
270
+ const entry = scriptMap.get(key) ?? { selfBytes: 0, functionIds: new Set };
271
+ entry.selfBytes += selfBytes;
272
+ entry.functionIds.add(nodeId);
273
+ scriptMap.set(key, entry);
274
+ }
275
+ hotspots.sort((a, b) => b.selfBytes - a.selfBytes);
276
+ const topAllocations = hotspots.slice(0, 50);
277
+ if (totalAllocatedBytes > 0) {
278
+ for (const h of topAllocations) {
279
+ h.selfPercent = round2(h.selfBytes / totalAllocatedBytes * 100);
280
+ }
281
+ }
282
+ const scriptBreakdown = Array.from(scriptMap.entries()).map(([scriptUrl, data]) => ({
283
+ scriptUrl,
284
+ selfBytes: data.selfBytes,
285
+ selfPercent: totalAllocatedBytes > 0 ? round2(data.selfBytes / totalAllocatedBytes * 100) : 0,
286
+ functionCount: data.functionIds.size
287
+ })).sort((a, b) => b.selfBytes - a.selfBytes);
288
+ return {
289
+ profilePath,
290
+ totalAllocatedBytes: Math.round(totalAllocatedBytes),
291
+ sampleCount: raw.samples?.length ?? 0,
292
+ topAllocations,
293
+ scriptBreakdown
294
+ };
295
+ }
296
+ function round2(n) {
297
+ return Math.round(n * 100) / 100;
298
+ }
299
+
300
+ // src/vitest/workspace.ts
301
+ import { FilesystemBackend } from "deepagents";
302
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
303
+ import { join } from "node:path";
304
+ import { tmpdir } from "node:os";
305
+ var SOURCE_INCLUSION_THRESHOLD = 1;
306
+ var SLOW_TEST_THRESHOLD = 100;
307
+ async function createVitestWorkspace(options) {
308
+ const { testTiming, profiles, heapProfiles, testSources, sourcePaths } = options;
309
+ const files = {};
310
+ const totalTests = testTiming.reduce((s, t) => s + t.testCount, 0);
311
+ const totalDuration = testTiming.reduce((s, t) => s + t.duration, 0);
312
+ const passCount = testTiming.reduce((s, t) => s + t.passCount, 0);
313
+ const failCount = testTiming.reduce((s, t) => s + t.failCount, 0);
314
+ const slowest = testTiming.length > 0 ? testTiming.reduce((a, b) => a.duration > b.duration ? a : b) : null;
315
+ const totalGcTime = profiles.reduce((s, p) => s + p.summary.duration * p.summary.gcPercentage / 100, 0);
316
+ const gcPercentage = totalDuration > 0 ? round3(totalGcTime / totalDuration * 100) : 0;
317
+ files["/summary.json"] = JSON.stringify({
318
+ totalTests,
319
+ totalDuration: round3(totalDuration),
320
+ passCount,
321
+ failCount,
322
+ profileCount: profiles.length,
323
+ slowestFile: slowest?.file ?? null,
324
+ slowestFileDuration: slowest ? round3(slowest.duration) : 0,
325
+ totalGcTime: round3(totalGcTime),
326
+ gcPercentage
327
+ }, null, 2);
328
+ files["/timing/overview.json"] = JSON.stringify(testTiming, null, 2);
329
+ const slowTests = [];
330
+ for (const fileTiming of testTiming) {
331
+ for (const test of fileTiming.tests) {
332
+ if (test.duration > SLOW_TEST_THRESHOLD) {
333
+ slowTests.push({
334
+ file: fileTiming.file,
335
+ name: test.name,
336
+ duration: test.duration
337
+ });
338
+ }
339
+ }
340
+ }
341
+ slowTests.sort((a, b) => b.duration - a.duration);
342
+ files["/timing/slow-tests.json"] = JSON.stringify(slowTests, null, 2);
343
+ files["/profiles/index.json"] = JSON.stringify(profiles.map((p) => ({
344
+ testFile: p.testFile,
345
+ profilePath: p.profilePath
346
+ })), null, 2);
347
+ for (const profile of profiles) {
348
+ const safeName = sanitizeFilename(profile.testFile);
349
+ files[`/profiles/${safeName}.json`] = JSON.stringify(profile.summary, null, 2);
350
+ }
351
+ if (heapProfiles && heapProfiles.length > 0) {
352
+ files["/heap-profiles/index.json"] = JSON.stringify(heapProfiles.map((p) => ({
353
+ testFile: p.testFile,
354
+ profilePath: p.profilePath
355
+ })), null, 2);
356
+ for (const hp of heapProfiles) {
357
+ const safeName = sanitizeFilename(hp.testFile);
358
+ files[`/heap-profiles/${safeName}.json`] = JSON.stringify(hp.summary, null, 2);
359
+ }
360
+ }
361
+ const mergedHotFunctions = mergeHotFunctions(profiles);
362
+ files["/hot-functions/global.json"] = JSON.stringify(mergedHotFunctions, null, 2);
363
+ const appHotFunctions = mergedHotFunctions.filter((fn) => fn.sourceCategory === "application");
364
+ files["/hot-functions/application.json"] = JSON.stringify(appHotFunctions, null, 2);
365
+ const depHotFunctions = mergedHotFunctions.filter((fn) => fn.sourceCategory === "dependency");
366
+ files["/hot-functions/dependencies.json"] = JSON.stringify(depHotFunctions, null, 2);
367
+ const appScripts = profiles.flatMap((p) => p.summary.scriptBreakdown.filter((s) => s.sourceCategory === "application"));
368
+ const appScriptMap = new Map;
369
+ for (const s of appScripts) {
370
+ const existing = appScriptMap.get(s.scriptUrl);
371
+ if (existing) {
372
+ existing.selfTime += s.selfTime;
373
+ existing.functionCount = Math.max(existing.functionCount, s.functionCount);
374
+ } else {
375
+ appScriptMap.set(s.scriptUrl, {
376
+ selfTime: s.selfTime,
377
+ functionCount: s.functionCount
378
+ });
379
+ }
380
+ }
381
+ const totalDurationMs = testTiming.reduce((s, t) => s + t.duration, 0);
382
+ const appScriptSummary = Array.from(appScriptMap.entries()).map(([scriptUrl, data]) => ({
383
+ scriptUrl,
384
+ selfTime: round3(data.selfTime),
385
+ selfPercent: totalDurationMs > 0 ? round3(data.selfTime / totalDurationMs * 100) : 0,
386
+ functionCount: data.functionCount
387
+ })).sort((a, b) => b.selfTime - a.selfTime);
388
+ files["/scripts/application.json"] = JSON.stringify(appScriptSummary, null, 2);
389
+ const depScripts = profiles.flatMap((p) => p.summary.scriptBreakdown.filter((s) => s.sourceCategory === "dependency"));
390
+ const depScriptMap = new Map;
391
+ for (const s of depScripts) {
392
+ const existing = depScriptMap.get(s.scriptUrl);
393
+ if (existing) {
394
+ existing.selfTime += s.selfTime;
395
+ existing.functionCount = Math.max(existing.functionCount, s.functionCount);
396
+ } else {
397
+ depScriptMap.set(s.scriptUrl, {
398
+ selfTime: s.selfTime,
399
+ functionCount: s.functionCount
400
+ });
401
+ }
402
+ }
403
+ const depScriptSummary = Array.from(depScriptMap.entries()).map(([scriptUrl, data]) => ({
404
+ scriptUrl,
405
+ selfTime: round3(data.selfTime),
406
+ selfPercent: totalDurationMs > 0 ? round3(data.selfTime / totalDurationMs * 100) : 0,
407
+ functionCount: data.functionCount
408
+ })).sort((a, b) => b.selfTime - a.selfTime);
409
+ files["/scripts/dependencies.json"] = JSON.stringify(depScriptSummary, null, 2);
410
+ for (const [filePath, source] of testSources) {
411
+ const filename = filePath.split("/").pop() ?? filePath;
412
+ files[`/tests/${filename}`] = source;
413
+ }
414
+ if (sourcePaths) {
415
+ const hotScriptUrls = new Set;
416
+ for (const profile of profiles) {
417
+ for (const fn of profile.summary.hotFunctions) {
418
+ if (!fn.scriptUrl)
419
+ continue;
420
+ const threshold = fn.sourceCategory === "application" ? 0.1 : SOURCE_INCLUSION_THRESHOLD;
421
+ if (fn.selfPercent >= threshold) {
422
+ hotScriptUrls.add(fn.scriptUrl);
423
+ }
424
+ }
425
+ }
426
+ for (const [scriptUrl, source] of sourcePaths) {
427
+ if (!hotScriptUrls.has(scriptUrl))
428
+ continue;
429
+ const filename = scriptUrl.split("/").pop() ?? scriptUrl;
430
+ files[`/src/${filename}`] = source;
431
+ }
432
+ }
433
+ const tempDir = mkdtempSync(join(tmpdir(), "zeitzeuge-vitest-workspace-"));
434
+ for (const [filePath, content] of Object.entries(files)) {
435
+ const relPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
436
+ const fullPath = join(tempDir, relPath);
437
+ mkdirSync(join(fullPath, ".."), { recursive: true });
438
+ writeFileSync(fullPath, content, "utf-8");
439
+ }
440
+ const backend = new FilesystemBackend({
441
+ rootDir: tempDir,
442
+ virtualMode: true
443
+ });
444
+ const cleanup = () => {
445
+ try {
446
+ const { rmSync } = __require("node:fs");
447
+ rmSync(tempDir, { recursive: true, force: true });
448
+ } catch {}
449
+ };
450
+ return { backend, cleanup };
451
+ }
452
+ function mergeHotFunctions(profiles) {
453
+ const merged = new Map;
454
+ let totalDuration = 0;
455
+ for (const profile of profiles) {
456
+ totalDuration += profile.summary.duration;
457
+ for (const fn of profile.summary.hotFunctions) {
458
+ const key = `${fn.scriptUrl}:${fn.functionName}:${fn.lineNumber}`;
459
+ const existing = merged.get(key);
460
+ if (existing) {
461
+ existing.selfTime += fn.selfTime;
462
+ existing.totalTime += fn.totalTime;
463
+ existing.hitCount += fn.hitCount;
464
+ } else {
465
+ merged.set(key, { ...fn });
466
+ }
467
+ }
468
+ }
469
+ if (totalDuration > 0) {
470
+ for (const fn of merged.values()) {
471
+ fn.selfPercent = round3(fn.selfTime / totalDuration * 100);
472
+ }
473
+ }
474
+ const results = Array.from(merged.values());
475
+ results.sort((a, b) => b.selfTime - a.selfTime);
476
+ return results.slice(0, 50);
477
+ }
478
+ function sanitizeFilename(filePath) {
479
+ return filePath.replace(/[/\\]/g, "_").replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^_+/, "");
480
+ }
481
+ function round3(n) {
482
+ return Math.round(n * 100) / 100;
483
+ }
484
+
485
+ // src/analysis/agent.ts
486
+ import { createDeepAgent } from "deepagents";
487
+ import { providerStrategy } from "langchain";
488
+
489
+ // src/analysis/prompts.ts
490
+ 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.
491
+
492
+ ## Workspace structure
493
+
494
+ - /heap/summary.json — Parsed V8 heap snapshot: largest objects, type stats, constructor stats, detached DOM nodes, closure stats
495
+ - /trace/summary.json — Page load metrics: timing, long tasks, render-blocking resources, resource breakdown
496
+ - /trace/network-waterfall.json — Every network request with timing, size, priority, render-blocking status
497
+ - /trace/asset-manifest.json — Index of all assets with paths to stored files
498
+ - /trace/runtime/summary.json — Runtime trace overview: frame breakdown (scripting/layout/paint/GC), blocking function count, listener imbalances, GC stats
499
+ - /trace/runtime/blocking-functions.json — Functions that blocked the main thread > 50ms, with script URL, line number, call stack, and duration
500
+ - /trace/runtime/event-listeners.json — Event listener add/remove counts per event type, with source locations
501
+ - /trace/runtime/frame-breakdown.json — Time spent in scripting vs layout vs paint vs GC
502
+ - /trace/runtime/raw-events.json — Full Chrome trace events (large file — read to investigate specific function calls, layouts, GC, and event dispatches)
503
+ - /scripts/*.js — Actual JavaScript source files captured during page load
504
+ - /styles/*.css — Actual CSS source files
505
+ - /html/document.html — The HTML document
506
+
507
+ ## Your workflow
508
+
509
+ 1. Read /heap/summary.json, /trace/summary.json, AND /trace/runtime/summary.json first for the big picture
510
+ 2. Identify the highest-impact issues from all datasets
511
+ 3. For each issue, dive into the relevant source files to understand the root cause
512
+ 4. Provide specific, code-level fixes
513
+
514
+ ## What to look for
515
+
516
+ ### Memory issues (from heap data)
517
+ - Memory leaks: unbounded arrays, maps, caches that grow without bound
518
+ - Detached DOM nodes: DOM elements removed from the document but still referenced
519
+ - Large retained objects: single objects or trees retaining disproportionate memory
520
+ - Closure leaks: closures capturing variables they no longer need
521
+
522
+ ### Page-load issues (from trace + source code)
523
+ - Render-blocking scripts: <script> in <head> without async/defer — read the script to judge if it must be synchronous
524
+ - Render-blocking CSS: large stylesheets blocking first paint
525
+ - Long tasks (> 50ms): identify the function/module causing the block by reading the source
526
+ - Large bundles: scripts > 100KB — search for unused imports or code that could be lazy-loaded
527
+ - Sequential waterfalls: resources chained sequentially that could load in parallel
528
+
529
+ ### Runtime issues (from Chrome trace)
530
+ - 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
531
+ - Event listener leaks: check /trace/runtime/event-listeners.json for event types where addCount >> removeCount, then grep the scripts for those addEventListener calls
532
+ - GC pressure: high GC pause counts or duration suggest excessive short-lived object creation — look for hot loops creating objects
533
+ - Layout thrashing: forced synchronous layouts caused by reading layout properties (offsetHeight, getBoundingClientRect) after DOM writes
534
+
535
+ ## Output guidelines
536
+
537
+ - Report 3–7 findings, ordered by impact (mix of memory, page-load, and runtime if all have issues)
538
+ - Be specific — name actual files, functions, object constructors, and retention paths
539
+ - Provide concrete code fixes, not generic advice
540
+ - If heap, trace, and runtime all look healthy, say so — don't manufacture issues`;
541
+
542
+ // src/vitest/prompts.ts
543
+ var VITEST_SYSTEM_PROMPT = `You are an expert in JavaScript/TypeScript performance optimization.
544
+ You have access to a workspace containing V8 CPU profiling data captured during
545
+ a Vitest test run. The workspace may also include V8 heap profiling data
546
+ captured via Node's \`--heap-prof\` (allocation sampling).
547
+
548
+ The profiling data covers BOTH the test code AND the application code being tested.
549
+
550
+ **Your primary goal is to analyze the PERFORMANCE OF THE APPLICATION CODE
551
+ being tested** — the functions, modules, and algorithms that the developer
552
+ wrote and is benchmarking or testing. Test infrastructure overhead (Vitest,
553
+ tinybench, test setup) is secondary context.
554
+
555
+ ## Source categories
556
+
557
+ Every hot function and script in the workspace has a \`sourceCategory\` field:
558
+
559
+ - **application** — Code in the user's project (the code being tested).
560
+ This is your PRIMARY focus. Find bottlenecks, inefficiencies, and
561
+ optimization opportunities in these functions.
562
+ - **dependency** — Third-party code in node_modules. Report when a dependency
563
+ is a significant bottleneck, since the developer may be able to choose
564
+ an alternative, configure it differently, or avoid calling it in hot paths.
565
+ - **test** — Test files. Only mention if the test setup itself is creating
566
+ artificial overhead that masks application performance.
567
+ - **framework** — Vitest/tinybench/V8 internals. Generally ignore unless
568
+ they dominate the profile in an unexpected way.
569
+
570
+ ## Workspace structure
571
+
572
+ - /summary.json — Overall test run: total tests, duration, pass/fail, GC stats
573
+ - /timing/overview.json — Per-file test durations and individual test times
574
+ - /timing/slow-tests.json — Tests exceeding the slow threshold
575
+ - /profiles/index.json — Manifest mapping test files to their CPU profiles
576
+ - /profiles/<file>.json — CPU profile summary: hot functions (with sourceCategory),
577
+ call trees, GC samples, script breakdown (with sourceCategory)
578
+ - /heap-profiles/index.json — (optional) Manifest mapping test files to heap profiles
579
+ - /heap-profiles/<file>.json — (optional) Heap profile summary: allocation hotspots,
580
+ per-script allocated bytes (with sourceCategory)
581
+ - /hot-functions/application.json — **START HERE**: Hot functions from application code only
582
+ - /hot-functions/dependencies.json — Hot functions from third-party dependencies
583
+ - /hot-functions/global.json — All hot functions across all categories
584
+ - /scripts/application.json — Per-script time breakdown for application code
585
+ - /scripts/dependencies.json — Per-script time breakdown for dependencies
586
+ - /tests/*.ts — Test source files
587
+ - /src/*.ts — Application and dependency source files referenced by hot functions
588
+
589
+ ## Your workflow
590
+
591
+ 1. Read /hot-functions/application.json FIRST — these are the application-level
592
+ bottlenecks the developer wants to optimize
593
+ 2. Read /scripts/application.json for the per-file view of application code time
594
+ 3. Read /hot-functions/dependencies.json for costly dependency calls
595
+ 4. If present, read /heap-profiles/index.json and /heap-profiles/<file>.json to identify
596
+ allocation hotspots (functions/scripts allocating lots of bytes)
597
+ 5. Read /summary.json and /timing/overview.json for the big picture
598
+ 6. Read CPU profiles in /profiles/ for detailed call trees of the slowest tests
599
+ 7. Read the actual source code in /src/ and /tests/ to understand root causes
600
+ 8. Provide specific, actionable fixes targeting the application code
601
+
602
+ ## What to look for
603
+
604
+ ### Application code bottlenecks (PRIMARY FOCUS)
605
+ - Functions with high self time — where is the application spending CPU?
606
+ - Expensive algorithms: O(n²) loops, unnecessary sorting, repeated work
607
+ - String/JSON operations: excessive serialization, string concatenation in loops
608
+ - Object allocation hotspots: functions creating many short-lived objects
609
+ - Synchronous blocking: file I/O, crypto, or compression in hot paths
610
+ - Redundant computation: values computed repeatedly that could be cached/memoized
611
+ - Data structure choices: using arrays where Maps/Sets would be O(1)
612
+
613
+ ### Dependency-related bottlenecks
614
+ - Dependencies consuming disproportionate CPU — suggest alternatives or configuration
615
+ - Unnecessary calls to expensive dependency APIs in hot paths
616
+ - Dependencies pulled in for simple operations that could be hand-written
617
+
618
+ ### GC pressure from application code
619
+ - Application functions creating many temporary objects in tight loops
620
+ - Large array/object allocations that could be pooled or reused
621
+ - Closures capturing large scopes unnecessarily
622
+
623
+ ### Allocation hotspots (from heap profiles, if present)
624
+ - Functions allocating a large share of total bytes (even if CPU isn't dominant)
625
+ - Scripts/modules responsible for most allocation — suggest caching, reuse, pooling,
626
+ or avoiding intermediate arrays/objects
627
+ - When allocation hotspots match CPU hotspots, prioritize fixes there first
628
+
629
+ ### Call chain analysis
630
+ - Trace expensive call trees to find which APPLICATION function triggers them
631
+ - Follow the call tree from application entry points down to the hot leaf functions
632
+ - Identify which application-level design decisions lead to the bottleneck
633
+
634
+ ### Test infrastructure (SECONDARY — only if impactful)
635
+ - Test setup creating artificial overhead that dwarfs application execution
636
+ - Benchmarks measuring setup cost instead of application performance
637
+ - Only mention if it prevents getting clean application performance data
638
+
639
+ ## Finding categories
640
+
641
+ Each finding MUST use one of these exact category values:
642
+
643
+ - **algorithm** — Inefficient algorithm: O(n²) loops, brute-force search, repeated work
644
+ - **serialization** — Excessive JSON.stringify/parse, string concatenation, encoding
645
+ - **allocation** — Excessive object/array creation causing GC pressure
646
+ - **event-handling** — Listener leaks, unbounded event handler accumulation
647
+ - **hot-function** — Generic CPU-hot function that doesn't fit a more specific category
648
+ - **gc-pressure** — High garbage collection overhead
649
+ - **listener-leak** — Event listeners not cleaned up properly
650
+ - **unnecessary-computation** — Redundant work that could be cached or eliminated
651
+ - **blocking-io** — Synchronous I/O or blocking operations in hot paths
652
+ - **dependency-bottleneck** — Expensive dependency call the developer can optimize
653
+ - **slow-test** — Test itself is slow due to setup or teardown
654
+ - **expensive-setup** — Costly test setup (beforeAll/beforeEach)
655
+ - **import-overhead** — Expensive module imports at test time
656
+ - **other** — Doesn't fit any of the above
657
+
658
+ Prefer more specific categories (algorithm, serialization, allocation, event-handling)
659
+ over generic ones (hot-function, other) when the root cause is clear.
660
+
661
+ ## Output guidelines
662
+
663
+ - Report 3–7 findings, ordered by impact ON THE APPLICATION CODE
664
+ - Focus findings on functions the developer CAN change (application code first,
665
+ then dependency usage patterns, then test structure)
666
+ - Be specific — name actual files, functions, line numbers from the source code
667
+ - Provide concrete code-level fixes, not generic advice
668
+ - When reporting a dependency bottleneck, explain what application code is
669
+ calling it and how the developer can reduce that cost
670
+ - If the application code is already efficient, say so — don't force findings
671
+ about test infrastructure just to fill the report`;
672
+
673
+ // src/schema.ts
674
+ import { z } from "zod";
675
+ var ALL_CATEGORIES = [
676
+ "memory-leak",
677
+ "large-retained-object",
678
+ "detached-dom",
679
+ "render-blocking",
680
+ "long-task",
681
+ "unused-code",
682
+ "waterfall-bottleneck",
683
+ "large-asset",
684
+ "frame-blocking-function",
685
+ "listener-leak",
686
+ "gc-pressure",
687
+ "slow-test",
688
+ "expensive-setup",
689
+ "hot-function",
690
+ "unnecessary-computation",
691
+ "import-overhead",
692
+ "dependency-bottleneck",
693
+ "algorithm",
694
+ "serialization",
695
+ "allocation",
696
+ "event-handling",
697
+ "blocking-io",
698
+ "other"
699
+ ];
700
+ var FindingSchema = z.object({
701
+ severity: z.enum(["critical", "warning", "info"]),
702
+ title: z.string().describe("Short title for the finding"),
703
+ description: z.string().describe("Detailed explanation of the issue"),
704
+ category: z.string().describe(`Category of the performance issue. Use one of: ${ALL_CATEGORIES.join(", ")}`),
705
+ resourceUrl: z.string().optional().describe("URL of the resource involved"),
706
+ workspacePath: z.string().optional().describe("Path in the VFS workspace"),
707
+ impactMs: z.number().optional().describe("Impact on page load time in ms"),
708
+ retainedSize: z.number().optional().describe("Retained heap size in bytes"),
709
+ retainerPath: z.array(z.string()).optional().describe("Object retention path in the heap"),
710
+ suggestedFix: z.string().describe("Code snippet or guidance to fix the issue"),
711
+ testFile: z.string().optional().describe("Test file path (for test performance findings)"),
712
+ hotFunction: z.object({
713
+ name: z.string(),
714
+ scriptUrl: z.string(),
715
+ lineNumber: z.number(),
716
+ selfTime: z.number(),
717
+ selfPercent: z.number()
718
+ }).optional().describe("Hot function details (for hot-function findings)")
719
+ });
720
+ var FindingsSchema = z.object({
721
+ findings: z.array(FindingSchema)
722
+ });
723
+
724
+ // src/analysis/agent.ts
725
+ async function analyze(model, backend) {
726
+ const agent = createDeepAgent({
727
+ model,
728
+ systemPrompt: SYSTEM_PROMPT,
729
+ backend,
730
+ responseFormat: providerStrategy(FindingsSchema)
731
+ });
732
+ const userMessage = [
733
+ "Analyze the frontend performance data in this workspace.",
734
+ "",
735
+ "The workspace contains heap snapshot data, page-load trace data, and Chrome runtime trace data.",
736
+ "",
737
+ "Start by reading /heap/summary.json, /trace/summary.json, and /trace/runtime/summary.json",
738
+ "to understand the overall picture. Then explore:",
739
+ "",
740
+ "- /trace/network-waterfall.json for request timing",
741
+ "- /trace/runtime/blocking-functions.json for function-level main thread blocking",
742
+ "- /trace/runtime/event-listeners.json for listener add/remove imbalances",
743
+ "- /trace/runtime/frame-breakdown.json for frame breakdown (scripting vs layout vs paint vs GC)",
744
+ "- /scripts/ for the actual JavaScript source code",
745
+ "- /styles/ for CSS source",
746
+ "- /html/document.html for the page markup",
747
+ "",
748
+ "Look for memory issues (from the heap data), page-load issues (from the network trace),",
749
+ "and runtime issues (from the Chrome trace — blocking functions, listener leaks, GC pressure).",
750
+ "When you find a problem, read the actual source file to provide a specific, code-level fix."
751
+ ].join(`
752
+ `);
753
+ const result = await agent.invoke({
754
+ messages: [{ role: "user", content: userMessage }]
755
+ });
756
+ return result.structuredResponse.findings;
757
+ }
758
+ async function analyzeTestPerformance(model, backend) {
759
+ const agent = createDeepAgent({
760
+ model,
761
+ systemPrompt: VITEST_SYSTEM_PROMPT,
762
+ backend,
763
+ responseFormat: providerStrategy(FindingsSchema)
764
+ });
765
+ const userMessage = [
766
+ "Analyze the performance of the APPLICATION CODE being tested in this Vitest workspace.",
767
+ "",
768
+ "Follow this order:",
769
+ "1. Read /hot-functions/application.json — these are the hotspots IN the user's own code",
770
+ "2. Read /scripts/application.json — per-file CPU time for application source files",
771
+ "3. Read /hot-functions/dependencies.json — expensive dependency calls",
772
+ "4. If present, read /heap-profiles/index.json and /heap-profiles/<file>.json for allocation hotspots",
773
+ "5. Read /summary.json and /timing/overview.json for the big picture",
774
+ "6. Read CPU profiles in /profiles/ for detailed call trees",
775
+ "7. Read source files in /src/ and /tests/ to understand root causes and propose code-level fixes",
776
+ "",
777
+ "Focus findings on the APPLICATION code — what can the developer change in their own codebase",
778
+ "to improve performance? Dependency bottlenecks are worth reporting if the developer can",
779
+ "reduce how they call the dependency or choose an alternative."
780
+ ].join(`
781
+ `);
782
+ const result = await agent.invoke({
783
+ messages: [{ role: "user", content: userMessage }]
784
+ });
785
+ return result.structuredResponse.findings;
786
+ }
787
+
788
+ // src/models/init.ts
789
+ import { ChatOpenAI } from "@langchain/openai";
790
+ import { ChatAnthropic } from "@langchain/anthropic";
791
+ function initModel() {
792
+ const modelOverride = process.env.ZEITZEUGE_MODEL;
793
+ const openaiKey = process.env.OPENAI_API_KEY;
794
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
795
+ if (openaiKey) {
796
+ return new ChatOpenAI({
797
+ model: modelOverride ?? "gpt-5.2",
798
+ apiKey: openaiKey
799
+ });
800
+ }
801
+ if (anthropicKey) {
802
+ return new ChatAnthropic({
803
+ model: modelOverride ?? "claude-opus-4-6",
804
+ apiKey: anthropicKey
805
+ });
806
+ }
807
+ throw new Error(`No API key found. Set OPENAI_API_KEY or ANTHROPIC_API_KEY in your environment.
808
+
809
+ ` + ` export OPENAI_API_KEY=sk-...
810
+ ` + ` # or
811
+ ` + ` export ANTHROPIC_API_KEY=sk-ant-...
812
+ `);
813
+ }
814
+
815
+ // src/output/terminal.ts
816
+ import chalk from "chalk";
817
+ import ora from "ora";
818
+ var SEVERITY_ICONS = {
819
+ critical: chalk.red("\uD83D\uDD34 CRITICAL"),
820
+ warning: chalk.yellow("\uD83D\uDFE1 WARNING"),
821
+ info: chalk.green("\uD83D\uDFE2 INFO")
822
+ };
823
+ var CATEGORY_LABELS = {
824
+ "memory-leak": "Memory Leak",
825
+ "large-retained-object": "Large Retained Object",
826
+ "detached-dom": "Detached DOM",
827
+ "render-blocking": "Render-Blocking",
828
+ "long-task": "Long Task",
829
+ "unused-code": "Unused Code",
830
+ "waterfall-bottleneck": "Waterfall Bottleneck",
831
+ "large-asset": "Large Asset",
832
+ "frame-blocking-function": "Frame-Blocking Function",
833
+ "listener-leak": "Listener Leak",
834
+ "gc-pressure": "GC Pressure",
835
+ "slow-test": "Slow Test",
836
+ "expensive-setup": "Expensive Setup",
837
+ "hot-function": "Hot Function",
838
+ "unnecessary-computation": "Unnecessary Computation",
839
+ "import-overhead": "Import Overhead",
840
+ "dependency-bottleneck": "Dependency Bottleneck",
841
+ algorithm: "Inefficient Algorithm",
842
+ serialization: "Serialization Overhead",
843
+ allocation: "Excessive Allocation",
844
+ "event-handling": "Event Handling",
845
+ "blocking-io": "Blocking I/O",
846
+ other: "Other"
847
+ };
848
+ function printHeader(url, version) {
849
+ const urlDisplay = url.length > 44 ? url.slice(0, 41) + "..." : url;
850
+ console.log(chalk.cyan(`
851
+ ┌${"─".repeat(57)}┐
852
+ ` + `│ zeitzeuge v${version.padEnd(44)}│
853
+ ` + `│ Analyzing: ${urlDisplay.padEnd(44)}│
854
+ ` + `└${"─".repeat(57)}┘
855
+ `));
856
+ }
857
+ function createSpinner(text) {
858
+ return ora({ text, color: "cyan" }).start();
859
+ }
860
+ function printFindings(findings) {
861
+ console.log(chalk.dim(`
862
+ ` + "━".repeat(58) + `
863
+ `));
864
+ if (findings.length === 0) {
865
+ console.log(chalk.green(` ✔ No significant performance issues found. Page looks healthy!
866
+ `));
867
+ console.log(chalk.dim("━".repeat(58)));
868
+ return;
869
+ }
870
+ for (const finding of findings) {
871
+ const icon = SEVERITY_ICONS[finding.severity];
872
+ const categoryLabel = CATEGORY_LABELS[finding.category] ?? finding.category;
873
+ console.log(`${icon} [${categoryLabel}]: ${chalk.bold(finding.title)}`);
874
+ if (finding.retainedSize != null) {
875
+ console.log(chalk.dim(` Retained size: ${formatBytes(finding.retainedSize)}`));
876
+ }
877
+ if (finding.impactMs != null) {
878
+ console.log(chalk.dim(` Impact: ${finding.impactMs.toFixed(0)}ms`));
879
+ }
880
+ if (finding.resourceUrl) {
881
+ console.log(chalk.dim(` Resource: ${finding.resourceUrl}`));
882
+ }
883
+ if (finding.retainerPath && finding.retainerPath.length > 0) {
884
+ console.log(chalk.dim(` Path: ${finding.retainerPath.join(" → ")}`));
885
+ }
886
+ if (finding.testFile) {
887
+ console.log(chalk.dim(` Test file: ${finding.testFile}`));
888
+ }
889
+ if (finding.hotFunction) {
890
+ const hf = finding.hotFunction;
891
+ console.log(chalk.dim(` Function: ${hf.name} at ${hf.scriptUrl}:${hf.lineNumber} (selfTime: ${hf.selfTime.toFixed(0)}ms, ${hf.selfPercent.toFixed(1)}%)`));
892
+ }
893
+ console.log(`
894
+ ${finding.description}
895
+ `);
896
+ if (finding.suggestedFix) {
897
+ console.log(chalk.dim(" Suggested fix:"));
898
+ const lines = finding.suggestedFix.split(`
899
+ `);
900
+ const boxWidth = Math.max(...lines.map((l) => l.length), 20) + 4;
901
+ console.log(chalk.dim(` ┌${"─".repeat(boxWidth)}┐`));
902
+ for (const line of lines) {
903
+ console.log(chalk.dim(" │ ") + chalk.white(line.padEnd(boxWidth - 2)) + chalk.dim(" │"));
904
+ }
905
+ console.log(chalk.dim(` └${"─".repeat(boxWidth)}┘`));
906
+ }
907
+ console.log();
908
+ }
909
+ console.log(chalk.dim("━".repeat(58)));
910
+ const counts = {
911
+ critical: findings.filter((f) => f.severity === "critical").length,
912
+ warning: findings.filter((f) => f.severity === "warning").length,
913
+ info: findings.filter((f) => f.severity === "info").length
914
+ };
915
+ console.log(`
916
+ Summary: ${chalk.red(`${counts.critical} critical`)}, ` + `${chalk.yellow(`${counts.warning} warning`)}, ` + `${chalk.green(`${counts.info} info`)}
917
+ `);
918
+ }
919
+ function printCaptureInfo(heapSummary, trace) {
920
+ console.log(chalk.dim(`Heap: ${formatBytes(heapSummary.metadata.totalSize)} | ` + `Nodes: ${heapSummary.metadata.nodeCount.toLocaleString()} | ` + `Requests: ${trace.networkRequests.length} | ` + `Long tasks: ${trace.metrics.longTasks.length}`));
921
+ }
922
+ function printError(err) {
923
+ const message = err instanceof Error ? err.message : String(err);
924
+ console.error(chalk.red(`
925
+ ✖ Error: ${message}
926
+ `));
927
+ }
928
+ function formatBytes(bytes) {
929
+ if (bytes < 1024)
930
+ return `${bytes} B`;
931
+ if (bytes < 1024 * 1024)
932
+ return `${(bytes / 1024).toFixed(1)} KB`;
933
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
934
+ }
935
+
936
+ // src/output/report.ts
937
+ import { writeFileSync as writeFileSync2 } from "node:fs";
938
+ var SEVERITY_EMOJI = {
939
+ critical: "\uD83D\uDD34",
940
+ warning: "\uD83D\uDFE1",
941
+ info: "ℹ️"
942
+ };
943
+ var CATEGORY_LABELS2 = {
944
+ "memory-leak": "Memory Leak",
945
+ "large-retained-object": "Large Retained Object",
946
+ "detached-dom": "Detached DOM",
947
+ "render-blocking": "Render-Blocking",
948
+ "long-task": "Long Task",
949
+ "unused-code": "Unused Code",
950
+ "waterfall-bottleneck": "Waterfall Bottleneck",
951
+ "large-asset": "Large Asset",
952
+ "frame-blocking-function": "Frame-Blocking Function",
953
+ "listener-leak": "Listener Leak",
954
+ "gc-pressure": "GC Pressure",
955
+ "slow-test": "Slow Test",
956
+ "expensive-setup": "Expensive Setup",
957
+ "hot-function": "Hot Function",
958
+ "unnecessary-computation": "Unnecessary Computation",
959
+ "import-overhead": "Import Overhead",
960
+ "dependency-bottleneck": "Dependency Bottleneck",
961
+ algorithm: "Inefficient Algorithm",
962
+ serialization: "Serialization Overhead",
963
+ allocation: "Excessive Allocation",
964
+ "event-handling": "Event Handling",
965
+ "blocking-io": "Blocking I/O",
966
+ other: "Other"
967
+ };
968
+ function writeReport(outputPath, options) {
969
+ const md = generateMarkdown(options);
970
+ writeFileSync2(outputPath, md, "utf-8");
971
+ return outputPath;
972
+ }
973
+ function generateMarkdown(options) {
974
+ const { url, version, findings, heapSummary, trace } = options;
975
+ const now = new Date;
976
+ const sections = [];
977
+ sections.push(`# Performance Report`);
978
+ sections.push("");
979
+ sections.push(`> **${url}** — analyzed ${now.toISOString().replace("T", " ").slice(0, 16)} UTC by zeitzeuge v${version}`);
980
+ sections.push("");
981
+ const totalTransfer = trace.networkRequests.reduce((s, r) => s + r.encodedSize, 0);
982
+ const loadSec = (trace.metrics.loadComplete / 1000).toFixed(1);
983
+ const fcpSec = (trace.metrics.firstContentfulPaint / 1000).toFixed(2);
984
+ const tbt = trace.metrics.totalBlockingTime.toFixed(0);
985
+ const heapSize = formatBytes(heapSummary.metadata.totalSize);
986
+ const reqCount = trace.networkRequests.length;
987
+ sections.push(`**Page load** ${loadSec}s · **FCP** ${fcpSec}s · **TBT** ${tbt}ms · ` + `**Heap** ${heapSize} · **${reqCount} requests** (${formatBytes(totalTransfer)} transferred)`);
988
+ sections.push("");
989
+ const counts = {
990
+ critical: findings.filter((f) => f.severity === "critical").length,
991
+ warning: findings.filter((f) => f.severity === "warning").length,
992
+ info: findings.filter((f) => f.severity === "info").length
993
+ };
994
+ if (findings.length === 0) {
995
+ sections.push(`## ✅ No issues found`);
996
+ sections.push("");
997
+ sections.push(`No significant performance problems were detected. ` + `The page loads in ${loadSec}s with ${tbt}ms of total blocking time — looking healthy.`);
998
+ sections.push("");
999
+ } else {
1000
+ sections.push(`**${findings.length} issues found** — ` + `${counts.critical} critical, ${counts.warning} warning, ${counts.info} info`);
1001
+ sections.push("");
1002
+ for (let i = 0;i < findings.length; i++) {
1003
+ const f = findings[i];
1004
+ if (!f)
1005
+ continue;
1006
+ const emoji = SEVERITY_EMOJI[f.severity];
1007
+ const categoryLabel = CATEGORY_LABELS2[f.category] ?? f.category;
1008
+ sections.push(`---`);
1009
+ sections.push("");
1010
+ sections.push(`## ${emoji} ${f.title}`);
1011
+ sections.push("");
1012
+ const context = [`**${categoryLabel}**`];
1013
+ if (f.impactMs != null)
1014
+ context.push(`${f.impactMs.toFixed(0)}ms impact`);
1015
+ if (f.retainedSize != null)
1016
+ context.push(`${formatBytes(f.retainedSize)} retained`);
1017
+ if (f.resourceUrl)
1018
+ context.push(`\`${f.resourceUrl}\``);
1019
+ sections.push(context.join(" · "));
1020
+ sections.push("");
1021
+ sections.push(f.description);
1022
+ sections.push("");
1023
+ if (f.suggestedFix) {
1024
+ sections.push(`### How to fix`);
1025
+ sections.push("");
1026
+ const looksLikeCode = f.suggestedFix.includes("{") || f.suggestedFix.includes(";") || f.suggestedFix.includes("=>") || f.suggestedFix.includes("import ") || f.suggestedFix.includes("function ");
1027
+ if (looksLikeCode) {
1028
+ sections.push("```js");
1029
+ sections.push(f.suggestedFix);
1030
+ sections.push("```");
1031
+ } else {
1032
+ sections.push(f.suggestedFix);
1033
+ }
1034
+ sections.push("");
1035
+ }
1036
+ if (f.retainerPath && f.retainerPath.length > 0) {
1037
+ sections.push(`*Retention path:* ${f.retainerPath.map((p) => `\`${p}\``).join(" → ")}`);
1038
+ sections.push("");
1039
+ }
1040
+ }
1041
+ }
1042
+ sections.push(`---`);
1043
+ sections.push("");
1044
+ sections.push(`*Generated by zeitzeuge v${version}*`);
1045
+ sections.push("");
1046
+ return sections.join(`
1047
+ `);
1048
+ }
1049
+ function writeTestReport(outputPath, options) {
1050
+ const md = generateTestMarkdown(options);
1051
+ writeFileSync2(outputPath, md, "utf-8");
1052
+ return outputPath;
1053
+ }
1054
+ function generateTestMarkdown(options) {
1055
+ const { version, findings, testTiming, profiles } = options;
1056
+ const now = new Date;
1057
+ const sections = [];
1058
+ sections.push(`# Vitest Performance Report`);
1059
+ sections.push("");
1060
+ sections.push(`> Analyzed ${now.toISOString().replace("T", " ").slice(0, 16)} UTC by zeitzeuge v${version}`);
1061
+ sections.push("");
1062
+ const totalTests = testTiming.reduce((s, t) => s + t.testCount, 0);
1063
+ const totalFiles = testTiming.length;
1064
+ const totalDuration = testTiming.reduce((s, t) => s + t.duration, 0);
1065
+ const slowest = testTiming.length > 0 ? testTiming.reduce((a, b) => a.duration > b.duration ? a : b) : null;
1066
+ const totalGcTime = profiles.reduce((s, p) => s + p.summary.duration * p.summary.gcPercentage / 100, 0);
1067
+ const gcPercentage = totalDuration > 0 ? (totalGcTime / totalDuration * 100).toFixed(2) : "0";
1068
+ sections.push(`**Test run** ${totalTests} tests across ${totalFiles} files · ` + `**Total duration** ${(totalDuration / 1000).toFixed(2)}s · ` + `**Slowest file** ${slowest ? `${slowest.file} (${(slowest.duration / 1000).toFixed(2)}s)` : "—"} · ` + `**GC overhead** ${gcPercentage}% (${totalGcTime.toFixed(0)}ms)`);
1069
+ sections.push("");
1070
+ const counts = {
1071
+ critical: findings.filter((f) => f.severity === "critical").length,
1072
+ warning: findings.filter((f) => f.severity === "warning").length,
1073
+ info: findings.filter((f) => f.severity === "info").length
1074
+ };
1075
+ if (findings.length === 0) {
1076
+ sections.push(`## ✅ No issues found`);
1077
+ sections.push("");
1078
+ sections.push(`No significant performance problems were detected. ` + `Tests complete in ${(totalDuration / 1000).toFixed(2)}s — looking healthy.`);
1079
+ sections.push("");
1080
+ } else {
1081
+ sections.push(`**${findings.length} issues found** — ` + `${counts.critical} critical, ${counts.warning} warning, ${counts.info} info`);
1082
+ sections.push("");
1083
+ for (const f of findings) {
1084
+ const emoji = SEVERITY_EMOJI[f.severity];
1085
+ const categoryLabel = CATEGORY_LABELS2[f.category] ?? f.category;
1086
+ sections.push(`---`);
1087
+ sections.push("");
1088
+ sections.push(`## ${emoji} ${f.title}`);
1089
+ sections.push("");
1090
+ const context = [`**${categoryLabel}**`];
1091
+ if (f.impactMs != null)
1092
+ context.push(`${f.impactMs.toFixed(0)}ms impact`);
1093
+ if (f.testFile)
1094
+ context.push(`\`${f.testFile}\``);
1095
+ if (f.hotFunction) {
1096
+ context.push(`\`${f.hotFunction.name}\` (${f.hotFunction.selfTime.toFixed(0)}ms, ${f.hotFunction.selfPercent.toFixed(1)}%)`);
1097
+ }
1098
+ if (f.resourceUrl)
1099
+ context.push(`\`${f.resourceUrl}\``);
1100
+ sections.push(context.join(" · "));
1101
+ sections.push("");
1102
+ sections.push(f.description);
1103
+ sections.push("");
1104
+ if (f.suggestedFix) {
1105
+ sections.push(`### How to fix`);
1106
+ sections.push("");
1107
+ const looksLikeCode = f.suggestedFix.includes("{") || f.suggestedFix.includes(";") || f.suggestedFix.includes("=>") || f.suggestedFix.includes("import ") || f.suggestedFix.includes("function ");
1108
+ if (looksLikeCode) {
1109
+ sections.push("```ts");
1110
+ sections.push(f.suggestedFix);
1111
+ sections.push("```");
1112
+ } else {
1113
+ sections.push(f.suggestedFix);
1114
+ }
1115
+ sections.push("");
1116
+ }
1117
+ }
1118
+ }
1119
+ sections.push(`---`);
1120
+ sections.push("");
1121
+ sections.push(`*Generated by zeitzeuge v${version}*`);
1122
+ sections.push("");
1123
+ return sections.join(`
1124
+ `);
1125
+ }
1126
+
1127
+ // src/vitest/classify.ts
1128
+ import { resolve, relative } from "node:path";
1129
+ var TEST_FILE_PATTERNS = [/\.test\./, /\.spec\./, /\.bench\./, /__tests__\//, /__mocks__\//];
1130
+ var FRAMEWORK_PATTERNS = [
1131
+ /\/vitest\//,
1132
+ /\/tinybench\//,
1133
+ /\/vite\//,
1134
+ /\/@vitest\//,
1135
+ /node:internal\//,
1136
+ /node:v8/,
1137
+ /node:worker_threads/,
1138
+ /\.XdZDrNZV\./,
1139
+ /\.CJqBMi0u\./
1140
+ ];
1141
+ function classifyScript(scriptUrl, projectRoot, testFiles) {
1142
+ if (!scriptUrl)
1143
+ return "unknown";
1144
+ let filePath = scriptUrl;
1145
+ if (filePath.startsWith("file://")) {
1146
+ try {
1147
+ filePath = new URL(filePath).pathname;
1148
+ } catch {}
1149
+ }
1150
+ if (filePath.startsWith("node:") || filePath.startsWith("v8:")) {
1151
+ return "framework";
1152
+ }
1153
+ for (const pattern of FRAMEWORK_PATTERNS) {
1154
+ if (pattern.test(filePath)) {
1155
+ return "framework";
1156
+ }
1157
+ }
1158
+ if (filePath.includes("/node_modules/") || filePath.includes("\\node_modules\\")) {
1159
+ return "dependency";
1160
+ }
1161
+ if (testFiles) {
1162
+ const resolved = resolve(filePath);
1163
+ if (testFiles.has(resolved)) {
1164
+ return "test";
1165
+ }
1166
+ }
1167
+ for (const pattern of TEST_FILE_PATTERNS) {
1168
+ if (pattern.test(filePath)) {
1169
+ return "test";
1170
+ }
1171
+ }
1172
+ const resolvedProject = resolve(projectRoot);
1173
+ const resolvedFile = resolve(filePath);
1174
+ const rel = relative(resolvedProject, resolvedFile);
1175
+ if (!rel.startsWith("..") && !rel.startsWith("/")) {
1176
+ return "application";
1177
+ }
1178
+ return "unknown";
1179
+ }
1180
+ // package.json
1181
+ var package_default = {
1182
+ name: "zeitzeuge",
1183
+ version: "0.3.0",
1184
+ description: "A deepagent to witnessing slowdowns in your test runs.",
1185
+ keywords: [
1186
+ "analysis",
1187
+ "deepagent",
1188
+ "frontend",
1189
+ "performance",
1190
+ "zeitzeuge"
1191
+ ],
1192
+ homepage: "https://github.com/christian-bromann/zeitzeuge",
1193
+ license: "MIT",
1194
+ author: "Christian Bromann <christian@bromann.dev>",
1195
+ repository: {
1196
+ type: "git",
1197
+ url: "https://github.com/christian-bromann/zeitzeuge.git"
1198
+ },
1199
+ bin: {
1200
+ zeitzeuge: "./dist/cli.js"
1201
+ },
1202
+ files: [
1203
+ "dist"
1204
+ ],
1205
+ type: "module",
1206
+ module: "src/cli.ts",
1207
+ types: "./dist/cli.d.ts",
1208
+ exports: {
1209
+ ".": {
1210
+ types: "./dist/cli.d.ts",
1211
+ default: "./dist/cli.js"
1212
+ },
1213
+ "./vitest": {
1214
+ types: "./dist/vitest/index.d.ts",
1215
+ default: "./dist/vitest/index.js"
1216
+ }
1217
+ },
1218
+ scripts: {
1219
+ "build:js": "bun build src/cli.ts src/vitest/index.ts --outdir dist --target node --format esm --packages external",
1220
+ "build:types": "bunx tsc -p tsconfig.build.json",
1221
+ build: "bun run build:js && bun run build:types",
1222
+ dev: "bun run src/cli.ts",
1223
+ fmt: "oxfmt",
1224
+ "fmt:check": "oxfmt --check",
1225
+ lint: "bun ./node_modules/oxlint/dist/cli.js",
1226
+ "lint:fix": "bun ./node_modules/oxlint/dist/cli.js --fix",
1227
+ test: "bun test"
1228
+ },
1229
+ dependencies: {
1230
+ "@langchain/anthropic": "^1.3.17",
1231
+ "@langchain/core": "^1.1.24",
1232
+ "@langchain/node-vfs": "^0.1",
1233
+ "@langchain/openai": "^1.2.7",
1234
+ chalk: "^5.6.2",
1235
+ deepagents: "^1.7.6",
1236
+ langchain: "^1.2.23",
1237
+ ora: "^9.3.0",
1238
+ "puppeteer-core": "^24.37.2",
1239
+ webdriverio: "^9.24.0",
1240
+ yargs: "^18.0.0",
1241
+ zod: "^4.3.6"
1242
+ },
1243
+ devDependencies: {
1244
+ "@types/bun": "latest",
1245
+ "@types/yargs": "^17",
1246
+ oxfmt: "^0.32.0",
1247
+ oxlint: "^1.47.0",
1248
+ typescript: "^5"
1249
+ },
1250
+ peerDependencies: {
1251
+ vitest: ">=3.1.0"
1252
+ },
1253
+ engines: {
1254
+ node: ">=24"
1255
+ },
1256
+ packageManager: "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8"
1257
+ };
1258
+
1259
+ // src/vitest/reporter.ts
1260
+ class ZeitZeugeReporter {
1261
+ options;
1262
+ executionOrder = [];
1263
+ isCI = !!process.env.CI;
1264
+ constructor(options) {
1265
+ this.options = options;
1266
+ }
1267
+ onTestModuleStart(testModule) {
1268
+ const filePath = testModule?.moduleId ?? testModule?.id ?? "";
1269
+ if (filePath && !this.executionOrder.includes(filePath)) {
1270
+ this.executionOrder.push(filePath);
1271
+ }
1272
+ }
1273
+ async onTestRunEnd(testModules) {
1274
+ try {
1275
+ await this.runAnalysis(testModules);
1276
+ } catch (err) {
1277
+ const message = err instanceof Error ? err.message : String(err);
1278
+ console.error(chalk2.red(`
1279
+ [zeitzeuge] Analysis failed: ${message}
1280
+ `));
1281
+ if (this.options.verbose && err instanceof Error) {
1282
+ console.error(err.stack);
1283
+ }
1284
+ } finally {
1285
+ this.cleanupProfileDir();
1286
+ }
1287
+ }
1288
+ async runAnalysis(testModules) {
1289
+ const testTiming = this.collectTestTiming(testModules);
1290
+ if (testTiming.length === 0) {
1291
+ if (this.options.verbose) {
1292
+ console.log("[zeitzeuge] No test modules found, skipping analysis.");
1293
+ }
1294
+ return;
1295
+ }
1296
+ const spinner = this.isCI ? null : ora2({ text: "zeitzeuge: Collecting CPU profiles...", color: "cyan" }).start();
1297
+ const profiles = this.collectAndParseProfiles(testTiming);
1298
+ const heapProfiles = this.collectAndParseHeapProfiles(testTiming);
1299
+ if (profiles.length === 0) {
1300
+ spinner?.warn("zeitzeuge: No .cpuprofile files found. " + "Try running with { verbose: true } for diagnostics.");
1301
+ return;
1302
+ }
1303
+ spinner?.succeed(`zeitzeuge: ${profiles.length} CPU profile(s) collected`);
1304
+ if (this.options.verbose && heapProfiles.length > 0) {
1305
+ console.log(`[zeitzeuge] ${heapProfiles.length} heap profile(s) collected`);
1306
+ }
1307
+ const testSources = this.readTestSources(testTiming);
1308
+ const sourcePaths = this.readHotFunctionSources(profiles);
1309
+ const wsSpinner = this.isCI ? null : ora2({ text: "zeitzeuge: Building analysis workspace...", color: "cyan" }).start();
1310
+ const workspace = await createVitestWorkspace({
1311
+ testTiming,
1312
+ profiles,
1313
+ heapProfiles,
1314
+ testSources,
1315
+ sourcePaths,
1316
+ projectRoot: this.options.projectRoot
1317
+ });
1318
+ wsSpinner?.succeed("zeitzeuge: Workspace ready");
1319
+ if (this.options.analyzeOnFinish) {
1320
+ const agentSpinner = this.isCI ? null : ora2({
1321
+ text: "zeitzeuge: Deep Agent analyzing test performance...",
1322
+ color: "cyan"
1323
+ }).start();
1324
+ try {
1325
+ const model = initModel();
1326
+ const findings = await analyzeTestPerformance(model, workspace.backend);
1327
+ agentSpinner?.succeed(`zeitzeuge: Analysis complete — ${findings.length} finding(s)`);
1328
+ console.log(chalk2.cyan(`
1329
+ ${"─".repeat(3)} zeitzeuge Performance Analysis ${"─".repeat(3)}
1330
+ `));
1331
+ printFindings(findings);
1332
+ const version = this.getVersion();
1333
+ const reportPath = writeTestReport(this.options.output, {
1334
+ version,
1335
+ findings,
1336
+ testTiming,
1337
+ profiles
1338
+ });
1339
+ console.log(`
1340
+ \uD83D\uDCC4 Report written to ${reportPath}
1341
+ `);
1342
+ } catch (err) {
1343
+ const message = err instanceof Error ? err.message : String(err);
1344
+ if (message.includes("API key") || message.includes("OPENAI_API_KEY") || message.includes("ANTHROPIC_API_KEY")) {
1345
+ agentSpinner?.warn("zeitzeuge: No LLM API key found. Set OPENAI_API_KEY or ANTHROPIC_API_KEY for AI-powered analysis.");
1346
+ } else {
1347
+ agentSpinner?.fail(`zeitzeuge: Analysis failed — ${message}`);
1348
+ throw err;
1349
+ }
1350
+ } finally {
1351
+ workspace.cleanup();
1352
+ }
1353
+ } else {
1354
+ workspace.cleanup();
1355
+ }
1356
+ }
1357
+ collectTestTiming(testModules) {
1358
+ const results = [];
1359
+ for (const mod of testModules) {
1360
+ const filePath = mod.moduleId ?? mod.id ?? "";
1361
+ if (!filePath)
1362
+ continue;
1363
+ const tests = [];
1364
+ let passCount = 0;
1365
+ let failCount = 0;
1366
+ if (mod.children) {
1367
+ this.walkTestCases(mod.children, tests);
1368
+ }
1369
+ for (const t of tests) {
1370
+ if (t.status === "pass")
1371
+ passCount++;
1372
+ else if (t.status === "fail")
1373
+ failCount++;
1374
+ }
1375
+ const diagnostic = typeof mod.diagnostic === "function" ? mod.diagnostic() : mod.diagnostic;
1376
+ const duration = diagnostic?.duration ?? tests.reduce((s, t) => s + t.duration, 0);
1377
+ const setupTime = diagnostic?.setupDuration ?? 0;
1378
+ results.push({
1379
+ file: filePath,
1380
+ duration,
1381
+ testCount: tests.length,
1382
+ passCount,
1383
+ failCount,
1384
+ setupTime,
1385
+ tests
1386
+ });
1387
+ }
1388
+ return results;
1389
+ }
1390
+ walkTestCases(children, results) {
1391
+ for (const child of children) {
1392
+ if (child.type === "test" || child.type === "case") {
1393
+ const diagnostic = typeof child.diagnostic === "function" ? child.diagnostic() : child.diagnostic;
1394
+ results.push({
1395
+ name: child.fullName ?? child.name ?? "",
1396
+ duration: diagnostic?.duration ?? 0,
1397
+ status: child.result?.state === "passed" ? "pass" : child.result?.state === "failed" ? "fail" : "skip"
1398
+ });
1399
+ }
1400
+ if (child.children) {
1401
+ this.walkTestCases(child.children, results);
1402
+ }
1403
+ }
1404
+ }
1405
+ collectAndParseProfiles(testTiming) {
1406
+ const { profileDir } = this.options;
1407
+ if (!existsSync(profileDir)) {
1408
+ if (this.options.verbose) {
1409
+ console.log(`[zeitzeuge] Profile directory not found: ${profileDir}`);
1410
+ }
1411
+ return [];
1412
+ }
1413
+ const allFiles = readdirSync(profileDir);
1414
+ if (this.options.verbose) {
1415
+ console.log(`[zeitzeuge] Profile directory ${profileDir} contains ${allFiles.length} file(s): ${allFiles.join(", ") || "(empty)"}`);
1416
+ }
1417
+ const profileFiles = allFiles.filter((f) => f.endsWith(".cpuprofile")).map((f) => {
1418
+ const fullPath = join2(profileDir, f);
1419
+ try {
1420
+ const stat = statSync(fullPath);
1421
+ return { name: f, path: fullPath, lastModified: stat.mtimeMs };
1422
+ } catch {
1423
+ return { name: f, path: fullPath, lastModified: 0 };
1424
+ }
1425
+ }).sort((a, b) => a.lastModified - b.lastModified);
1426
+ if (profileFiles.length === 0) {
1427
+ if (this.options.verbose) {
1428
+ console.log(`[zeitzeuge] No .cpuprofile files in ${profileDir}. ` + `This usually means --cpu-prof wasn't passed to the worker process. ` + `Check that pool is set to 'forks' and execArgv includes '--cpu-prof'.`);
1429
+ }
1430
+ return [];
1431
+ }
1432
+ const results = [];
1433
+ const orderedTestFiles = this.executionOrder.length > 0 ? this.executionOrder : testTiming.map((t) => t.file);
1434
+ for (let i = 0;i < profileFiles.length; i++) {
1435
+ const pf = profileFiles[i];
1436
+ const testFile = orderedTestFiles[i] ?? `unknown-${i}`;
1437
+ try {
1438
+ const content = readFileSync(pf.path, "utf-8");
1439
+ const rawProfile = JSON.parse(content);
1440
+ const summary = parseCpuProfile(rawProfile, pf.path);
1441
+ const testFileSet = new Set(testTiming.map((t) => resolve2(t.file)));
1442
+ for (const fn of summary.hotFunctions) {
1443
+ fn.sourceCategory = classifyScript(fn.scriptUrl, this.options.projectRoot, testFileSet);
1444
+ }
1445
+ for (const script of summary.scriptBreakdown) {
1446
+ script.sourceCategory = classifyScript(script.scriptUrl, this.options.projectRoot, testFileSet);
1447
+ }
1448
+ results.push({
1449
+ testFile,
1450
+ profilePath: pf.path,
1451
+ summary
1452
+ });
1453
+ } catch (err) {
1454
+ if (this.options.verbose) {
1455
+ console.warn(`[zeitzeuge] Failed to parse profile ${pf.name}: ${err instanceof Error ? err.message : err}`);
1456
+ }
1457
+ }
1458
+ }
1459
+ results.sort((a, b) => b.summary.duration - a.summary.duration);
1460
+ return results.slice(0, 10);
1461
+ }
1462
+ collectAndParseHeapProfiles(testTiming) {
1463
+ const { profileDir } = this.options;
1464
+ if (!existsSync(profileDir)) {
1465
+ return [];
1466
+ }
1467
+ const allFiles = readdirSync(profileDir);
1468
+ const heapFiles = allFiles.filter((f) => f.endsWith(".heapprofile")).map((f) => {
1469
+ const fullPath = join2(profileDir, f);
1470
+ try {
1471
+ const stat = statSync(fullPath);
1472
+ return { name: f, path: fullPath, lastModified: stat.mtimeMs };
1473
+ } catch {
1474
+ return { name: f, path: fullPath, lastModified: 0 };
1475
+ }
1476
+ }).sort((a, b) => a.lastModified - b.lastModified);
1477
+ if (heapFiles.length === 0) {
1478
+ return [];
1479
+ }
1480
+ const orderedTestFiles = this.executionOrder.length > 0 ? this.executionOrder : testTiming.map((t) => t.file);
1481
+ const results = [];
1482
+ const testFileSet = new Set(testTiming.map((t) => resolve2(t.file)));
1483
+ for (let i = 0;i < heapFiles.length; i++) {
1484
+ const hf = heapFiles[i];
1485
+ const testFile = orderedTestFiles[i] ?? `unknown-${i}`;
1486
+ try {
1487
+ const content = readFileSync(hf.path, "utf-8");
1488
+ const rawHeapProfile = JSON.parse(content);
1489
+ const summary = parseHeapProfile(rawHeapProfile, hf.path);
1490
+ for (const fn of summary.topAllocations) {
1491
+ fn.sourceCategory = classifyScript(fn.scriptUrl, this.options.projectRoot, testFileSet);
1492
+ }
1493
+ for (const script of summary.scriptBreakdown) {
1494
+ script.sourceCategory = classifyScript(script.scriptUrl, this.options.projectRoot, testFileSet);
1495
+ }
1496
+ results.push({
1497
+ testFile,
1498
+ profilePath: hf.path,
1499
+ summary
1500
+ });
1501
+ } catch (err) {
1502
+ if (this.options.verbose) {
1503
+ console.warn(`[zeitzeuge] Failed to parse heap profile ${hf.name}: ${err instanceof Error ? err.message : err}`);
1504
+ }
1505
+ }
1506
+ }
1507
+ return results.slice(0, 10);
1508
+ }
1509
+ readTestSources(testTiming) {
1510
+ const sources = new Map;
1511
+ for (const timing of testTiming) {
1512
+ try {
1513
+ const resolvedPath = resolve2(timing.file);
1514
+ if (existsSync(resolvedPath)) {
1515
+ sources.set(timing.file, readFileSync(resolvedPath, "utf-8"));
1516
+ }
1517
+ } catch {}
1518
+ }
1519
+ return sources;
1520
+ }
1521
+ readHotFunctionSources(profiles) {
1522
+ const sources = new Map;
1523
+ const seen = new Set;
1524
+ for (const profile of profiles) {
1525
+ for (const fn of profile.summary.hotFunctions) {
1526
+ if (!fn.scriptUrl || seen.has(fn.scriptUrl)) {
1527
+ continue;
1528
+ }
1529
+ const threshold = fn.sourceCategory === "application" ? 0.1 : 1;
1530
+ if (fn.selfPercent < threshold)
1531
+ continue;
1532
+ seen.add(fn.scriptUrl);
1533
+ try {
1534
+ let filePath = fn.scriptUrl;
1535
+ if (filePath.startsWith("file://")) {
1536
+ filePath = new URL(filePath).pathname;
1537
+ }
1538
+ if (existsSync(filePath)) {
1539
+ sources.set(fn.scriptUrl, readFileSync(filePath, "utf-8"));
1540
+ }
1541
+ } catch {}
1542
+ }
1543
+ }
1544
+ return sources;
1545
+ }
1546
+ cleanupProfileDir() {
1547
+ try {
1548
+ if (existsSync(this.options.profileDir)) {
1549
+ rmSync(this.options.profileDir, { recursive: true, force: true });
1550
+ }
1551
+ } catch {}
1552
+ }
1553
+ getVersion() {
1554
+ return package_default.version ?? "(unknown)";
1555
+ }
1556
+ }
1557
+
1558
+ // src/vitest/plugin.ts
1559
+ function zeitzeuge(options = {}) {
1560
+ const {
1561
+ enabled = true,
1562
+ output = "zeitzeuge-report.md",
1563
+ profileDir = ".zeitzeuge-profiles",
1564
+ heapProf = false,
1565
+ analyzeOnFinish = true,
1566
+ verbose = false,
1567
+ projectRoot = process.cwd()
1568
+ } = options;
1569
+ return {
1570
+ name: "vitest:zeitzeuge",
1571
+ configureVitest(context) {
1572
+ if (!enabled)
1573
+ return;
1574
+ const { vitest } = context;
1575
+ const resolvedProfileDir = resolve3(profileDir);
1576
+ try {
1577
+ mkdirSync2(resolvedProfileDir, { recursive: true });
1578
+ } catch {}
1579
+ const cpuProfArgs = ["--cpu-prof", `--cpu-prof-dir=${resolvedProfileDir}`];
1580
+ const heapProfArgs = heapProf ? ["--heap-prof", `--heap-prof-dir=${resolvedProfileDir}`] : [];
1581
+ const profilingArgs = [...cpuProfArgs, ...heapProfArgs];
1582
+ const existingArgv = vitest.config.execArgv ?? [];
1583
+ vitest.config.execArgv = [...existingArgv, ...profilingArgs];
1584
+ vitest.config.pool = "forks";
1585
+ if (!vitest.config.poolOptions) {
1586
+ vitest.config.poolOptions = {};
1587
+ }
1588
+ if (!vitest.config.poolOptions.forks) {
1589
+ vitest.config.poolOptions.forks = {};
1590
+ }
1591
+ const existingForksArgv = vitest.config.poolOptions.forks.execArgv ?? [];
1592
+ vitest.config.poolOptions.forks.execArgv = [...existingForksArgv, ...profilingArgs];
1593
+ vitest.config.fileParallelism = false;
1594
+ const reporter = new ZeitZeugeReporter({
1595
+ output: resolve3(output),
1596
+ profileDir: resolvedProfileDir,
1597
+ analyzeOnFinish,
1598
+ verbose,
1599
+ projectRoot: resolve3(projectRoot)
1600
+ });
1601
+ if (Array.isArray(vitest.config.reporters)) {
1602
+ vitest.config.reporters.push(reporter);
1603
+ } else {
1604
+ vitest.config.reporters = [reporter];
1605
+ }
1606
+ if (verbose) {
1607
+ console.log(`[zeitzeuge] Plugin enabled — CPU profiling to ${resolvedProfileDir}`);
1608
+ console.log(`[zeitzeuge] execArgv: ${JSON.stringify(vitest.config.execArgv)}`);
1609
+ console.log(`[zeitzeuge] pool: ${vitest.config.pool}`);
1610
+ }
1611
+ }
1612
+ };
1613
+ }
1614
+ export {
1615
+ zeitzeuge
1616
+ };