@mtharrison/pkg-profiler 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -52
- package/dist/index.cjs +7 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/reporter/aggregate.ts +2 -0
- package/src/reporter/html.ts +4 -2
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -1,91 +1,63 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
3
|
-
<rect x="10" y="50" width="22" height="60" rx="4" fill="#6366f1" opacity="0.5"/>
|
|
4
|
-
<rect x="38" y="30" width="22" height="80" rx="4" fill="#6366f1" opacity="0.7"/>
|
|
5
|
-
<rect x="66" y="10" width="22" height="100" rx="4" fill="#6366f1" opacity="0.9"/>
|
|
6
|
-
<circle cx="21" cy="40" r="6" fill="#f59e0b"/>
|
|
7
|
-
<circle cx="49" cy="22" r="6" fill="#f59e0b"/>
|
|
8
|
-
<circle cx="77" cy="6" r="6" fill="#f59e0b"/>
|
|
9
|
-
<line x1="21" y1="40" x2="49" y2="22" stroke="#f59e0b" stroke-width="2"/>
|
|
10
|
-
<line x1="49" y1="22" x2="77" y2="6" stroke="#f59e0b" stroke-width="2"/>
|
|
11
|
-
<rect x="94" y="45" width="16" height="12" rx="2" fill="#6366f1"/>
|
|
12
|
-
<rect x="94" y="61" width="16" height="12" rx="2" fill="#6366f1" opacity="0.7"/>
|
|
13
|
-
<rect x="94" y="77" width="16" height="12" rx="2" fill="#6366f1" opacity="0.4"/>
|
|
14
|
-
</svg>
|
|
2
|
+
<img src="assets/logo.svg" width="120" height="120" alt="where-you-at logo">
|
|
15
3
|
</p>
|
|
16
4
|
|
|
17
5
|
<h1 align="center">@mtharrison/pkg-profiler</h1>
|
|
18
6
|
|
|
19
7
|
<p align="center">
|
|
20
|
-
|
|
8
|
+
<strong>Where's your wall time going? Find out in one call.</strong><br>
|
|
9
|
+
Zero-dependency sampling profiler that breaks down Node.js wall time by npm package.
|
|
21
10
|
</p>
|
|
22
11
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
What you really want to know is: **is the bottleneck in my code, or in a dependency?** And if it's a dependency, *which one*?
|
|
30
|
-
|
|
31
|
-
`@mtharrison/pkg-profiler` gives you a per-package wall-time breakdown so you can instantly see whether you should be optimizing your own code or looking for a faster alternative to that one heavy dependency.
|
|
32
|
-
|
|
33
|
-
## Installation
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@mtharrison/pkg-profiler"><img src="https://img.shields.io/npm/v/@mtharrison/pkg-profiler" alt="npm version"></a>
|
|
14
|
+
<img src="https://img.shields.io/node/v/@mtharrison/pkg-profiler" alt="node version">
|
|
15
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/@mtharrison/pkg-profiler" alt="license"></a>
|
|
16
|
+
</p>
|
|
34
17
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img src="assets/report-screenshot.png" width="660" alt="Example HTML report showing per-package wall time breakdown">
|
|
20
|
+
</p>
|
|
38
21
|
|
|
39
|
-
##
|
|
22
|
+
## Quick Start
|
|
40
23
|
|
|
41
24
|
```typescript
|
|
42
25
|
import { track, report } from '@mtharrison/pkg-profiler';
|
|
43
26
|
|
|
44
27
|
await track();
|
|
45
|
-
|
|
46
28
|
// ... your code here ...
|
|
47
|
-
|
|
48
|
-
const reportPath = await report();
|
|
49
|
-
console.log(`Report written to ${reportPath}`);
|
|
29
|
+
const path = await report(); // writes an HTML report to cwd
|
|
50
30
|
```
|
|
51
31
|
|
|
52
|
-
|
|
32
|
+
## What You Get
|
|
33
|
+
|
|
34
|
+
A self-contained HTML report that shows exactly which npm packages are eating your wall time. The summary table gives you the top-level picture; expand the tree to drill into individual files and functions. First-party code is highlighted so you can instantly see whether the bottleneck is yours or a dependency's.
|
|
53
35
|
|
|
54
36
|
## API
|
|
55
37
|
|
|
56
38
|
### `track(options?)`
|
|
57
39
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
**Options:**
|
|
40
|
+
Start the V8 CPU sampling profiler. Safe no-op if already profiling.
|
|
61
41
|
|
|
62
|
-
| Option | Type | Description
|
|
63
|
-
|
|
64
|
-
| `interval` | `number` | Sampling interval in microseconds
|
|
65
|
-
|
|
66
|
-
**Returns:** `Promise<void>`
|
|
42
|
+
| Option | Type | Default | Description |
|
|
43
|
+
|------------|----------|---------|------------------------------------|
|
|
44
|
+
| `interval` | `number` | V8 default | Sampling interval in microseconds |
|
|
67
45
|
|
|
68
46
|
### `report()`
|
|
69
47
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Returns an empty string if no samples were collected.
|
|
73
|
-
|
|
74
|
-
**Returns:** `Promise<string>` -- absolute path to the generated HTML report
|
|
48
|
+
Stop profiling, generate an HTML report, write it to the current directory, and return the absolute file path. Resets all data afterward. Returns `""` if no samples were collected.
|
|
75
49
|
|
|
76
50
|
### `clear()`
|
|
77
51
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
**Returns:** `Promise<void>`
|
|
52
|
+
Stop profiling and discard all data without writing a report.
|
|
81
53
|
|
|
82
54
|
## How It Works
|
|
83
55
|
|
|
84
|
-
|
|
56
|
+
Uses the V8 CPU profiler (`node:inspector`) to sample the call stack at regular intervals. Each sample's leaf frame is attributed the elapsed wall time, then file paths are resolved to npm packages by walking up through `node_modules`. No code instrumentation required.
|
|
85
57
|
|
|
86
58
|
## Requirements
|
|
87
59
|
|
|
88
|
-
|
|
60
|
+
Node.js >= 20.0.0
|
|
89
61
|
|
|
90
62
|
## License
|
|
91
63
|
|
package/dist/index.cjs
CHANGED
|
@@ -114,7 +114,8 @@ function aggregate(store, projectName) {
|
|
|
114
114
|
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
115
115
|
totalTimeUs: 0,
|
|
116
116
|
packages: [],
|
|
117
|
-
otherCount: 0
|
|
117
|
+
otherCount: 0,
|
|
118
|
+
projectName
|
|
118
119
|
};
|
|
119
120
|
const threshold = totalTimeUs * THRESHOLD_PCT;
|
|
120
121
|
const packages = [];
|
|
@@ -182,7 +183,8 @@ function aggregate(store, projectName) {
|
|
|
182
183
|
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
183
184
|
totalTimeUs,
|
|
184
185
|
packages,
|
|
185
|
-
otherCount: topLevelOtherCount
|
|
186
|
+
otherCount: topLevelOtherCount,
|
|
187
|
+
projectName
|
|
186
188
|
};
|
|
187
189
|
}
|
|
188
190
|
|
|
@@ -536,17 +538,18 @@ function renderHtml(data) {
|
|
|
536
538
|
const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs);
|
|
537
539
|
const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs);
|
|
538
540
|
const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));
|
|
541
|
+
const titleName = escapeHtml(data.projectName);
|
|
539
542
|
return `<!DOCTYPE html>
|
|
540
543
|
<html lang="en">
|
|
541
544
|
<head>
|
|
542
545
|
<meta charset="utf-8">
|
|
543
546
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
544
|
-
<title
|
|
547
|
+
<title>${titleName} · where-you-at report</title>
|
|
545
548
|
<style>${generateCss()}
|
|
546
549
|
</style>
|
|
547
550
|
</head>
|
|
548
551
|
<body>
|
|
549
|
-
<h1
|
|
552
|
+
<h1>${titleName}</h1>
|
|
550
553
|
<div class="meta">Generated ${escapeHtml(data.timestamp)} · Total wall time: ${totalFormatted}</div>
|
|
551
554
|
|
|
552
555
|
<h2>Summary</h2>
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["sep","Session"],"sources":["../src/frame-parser.ts","../src/package-resolver.ts","../src/reporter/aggregate.ts","../src/reporter/format.ts","../src/reporter/html.ts","../src/reporter.ts","../src/sample-store.ts","../src/sampler.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport type { RawCallFrame, ParsedFrame } from './types.js';\n\n/**\n * Classify a V8 CPU profiler call frame and convert its URL to a filesystem path.\n *\n * Every sampled frame from the V8 profiler passes through this function first.\n * It determines the frame kind (user code, internal, eval, wasm) and for user\n * frames converts the URL to a filesystem path and builds a human-readable\n * function identifier.\n */\nexport function parseFrame(frame: RawCallFrame): ParsedFrame {\n const { url, functionName, lineNumber } = frame;\n\n // Empty URL: V8 internal pseudo-frames like (idle), (root), (gc), (program)\n if (url === '') {\n return { kind: 'internal' };\n }\n\n // Node.js built-in modules -- treated as attributable user frames\n if (url.startsWith('node:')) {\n const functionId = `${functionName || '<anonymous>'}:${lineNumber + 1}`;\n return { kind: 'user', filePath: url, functionId };\n }\n\n // WebAssembly frames\n if (url.startsWith('wasm:')) {\n return { kind: 'wasm' };\n }\n\n // Eval frames\n if (url.includes('eval')) {\n return { kind: 'eval' };\n }\n\n // User code: convert URL to filesystem path\n const filePath = url.startsWith('file://')\n ? fileURLToPath(url)\n : url;\n\n // Build human-readable function identifier (convert 0-based to 1-based line)\n const functionId = `${functionName || '<anonymous>'}:${lineNumber + 1}`;\n\n return { kind: 'user', filePath, functionId };\n}\n","import { readFileSync } from 'node:fs';\nimport { dirname, join, relative, sep } from 'node:path';\n\n/**\n * Resolves an absolute file path to a package name and relative path.\n *\n * For node_modules paths: extracts the package name from the last /node_modules/\n * segment (critical for pnpm virtual store compatibility). Handles scoped packages.\n *\n * For first-party files: walks up directory tree looking for package.json,\n * falls back to 'app' if none found.\n */\nexport class PackageResolver {\n private readonly projectRoot: string;\n private readonly packageJsonCache = new Map<string, string | null>();\n\n constructor(projectRoot: string) {\n this.projectRoot = projectRoot;\n }\n\n resolve(absoluteFilePath: string): { packageName: string; relativePath: string } {\n const nodeModulesSeg = `${sep}node_modules${sep}`;\n const lastIdx = absoluteFilePath.lastIndexOf(nodeModulesSeg);\n\n if (lastIdx !== -1) {\n // node_modules path -- extract package name from LAST /node_modules/ segment\n const afterModules = absoluteFilePath.substring(lastIdx + nodeModulesSeg.length);\n const segments = afterModules.split(sep);\n\n let packageName: string;\n let fileStartIdx: number;\n\n if (segments[0]!.startsWith('@')) {\n // Scoped package: @scope/name\n packageName = `${segments[0]}/${segments[1]}`;\n fileStartIdx = 2;\n } else {\n packageName = segments[0]!;\n fileStartIdx = 1;\n }\n\n const relativePath = segments.slice(fileStartIdx).join('/');\n\n return { packageName, relativePath };\n }\n\n // First-party file -- walk up looking for package.json\n const packageName = this.findPackageName(absoluteFilePath);\n const relativePath = relative(this.projectRoot, absoluteFilePath)\n .split(sep)\n .join('/');\n\n return { packageName, relativePath };\n }\n\n /**\n * Walk up from the file's directory looking for package.json.\n * Cache results to avoid repeated filesystem reads.\n */\n private findPackageName(absoluteFilePath: string): string {\n let dir = dirname(absoluteFilePath);\n\n while (true) {\n const cached = this.packageJsonCache.get(dir);\n if (cached !== undefined) {\n if (cached !== null) {\n return cached;\n }\n // null means we checked this dir and no package.json -- continue up\n } else {\n try {\n const raw = readFileSync(join(dir, 'package.json'), 'utf-8');\n const pkg = JSON.parse(raw) as { name?: string };\n const name = pkg.name ?? null;\n this.packageJsonCache.set(dir, name);\n if (name !== null) {\n return name;\n }\n } catch {\n // No package.json here -- cache as null and continue\n this.packageJsonCache.set(dir, null);\n }\n }\n\n const parent = dirname(dir);\n if (parent === dir) {\n // Reached filesystem root without finding a named package.json\n return 'app';\n }\n dir = parent;\n }\n }\n}\n","/**\n * Transform raw SampleStore data into a sorted, thresholded ReportData structure.\n *\n * Pure function: reads nested Maps from SampleStore, applies a 5% threshold\n * at every level (package, file, function) relative to the total profiled time,\n * aggregates below-threshold entries into an \"other\" count, and sorts by\n * timeUs descending at each level.\n */\n\nimport type { SampleStore } from '../sample-store.js';\nimport type {\n ReportData,\n PackageEntry,\n FileEntry,\n FunctionEntry,\n} from '../types.js';\n\nconst THRESHOLD_PCT = 0.05;\n\n/**\n * Aggregate SampleStore data into a ReportData structure.\n *\n * @param store - SampleStore with accumulated microseconds and sample counts\n * @param projectName - Name of the first-party project (for isFirstParty flag)\n * @returns ReportData with packages sorted desc by time, thresholded at 5%\n */\nexport function aggregate(store: SampleStore, projectName: string): ReportData {\n // 1. Calculate total user-attributed time\n let totalTimeUs = 0;\n for (const fileMap of store.packages.values()) {\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n totalTimeUs += us;\n }\n }\n }\n\n if (totalTimeUs === 0) {\n return {\n timestamp: new Date().toLocaleString(),\n totalTimeUs: 0,\n packages: [],\n otherCount: 0,\n };\n }\n\n const threshold = totalTimeUs * THRESHOLD_PCT;\n const packages: PackageEntry[] = [];\n let topLevelOtherCount = 0;\n\n // 2. Process each package\n for (const [packageName, fileMap] of store.packages) {\n // Sum total time for this package\n let packageTimeUs = 0;\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n packageTimeUs += us;\n }\n }\n\n // Apply threshold at package level\n if (packageTimeUs < threshold) {\n topLevelOtherCount++;\n continue;\n }\n\n // Sum total sample count for this package\n let packageSampleCount = 0;\n const countFileMap = store.sampleCountsByPackage.get(packageName);\n if (countFileMap) {\n for (const countFuncMap of countFileMap.values()) {\n for (const count of countFuncMap.values()) {\n packageSampleCount += count;\n }\n }\n }\n\n // 3. Process files within the package\n const files: FileEntry[] = [];\n let fileOtherCount = 0;\n\n for (const [fileName, funcMap] of fileMap) {\n // Sum time for this file\n let fileTimeUs = 0;\n for (const us of funcMap.values()) {\n fileTimeUs += us;\n }\n\n // Apply threshold at file level (relative to total)\n if (fileTimeUs < threshold) {\n fileOtherCount++;\n continue;\n }\n\n // Sum sample count for this file\n let fileSampleCount = 0;\n const countFuncMap = countFileMap?.get(fileName);\n if (countFuncMap) {\n for (const count of countFuncMap.values()) {\n fileSampleCount += count;\n }\n }\n\n // 4. Process functions within the file\n const functions: FunctionEntry[] = [];\n let funcOtherCount = 0;\n\n for (const [funcName, funcTimeUs] of funcMap) {\n // Apply threshold at function level (relative to total)\n if (funcTimeUs < threshold) {\n funcOtherCount++;\n continue;\n }\n\n const funcSampleCount = countFuncMap?.get(funcName) ?? 0;\n\n functions.push({\n name: funcName,\n timeUs: funcTimeUs,\n pct: (funcTimeUs / totalTimeUs) * 100,\n sampleCount: funcSampleCount,\n });\n }\n\n // Sort functions by timeUs descending\n functions.sort((a, b) => b.timeUs - a.timeUs);\n\n files.push({\n name: fileName,\n timeUs: fileTimeUs,\n pct: (fileTimeUs / totalTimeUs) * 100,\n sampleCount: fileSampleCount,\n functions,\n otherCount: funcOtherCount,\n });\n }\n\n // Sort files by timeUs descending\n files.sort((a, b) => b.timeUs - a.timeUs);\n\n packages.push({\n name: packageName,\n timeUs: packageTimeUs,\n pct: (packageTimeUs / totalTimeUs) * 100,\n isFirstParty: packageName === projectName,\n sampleCount: packageSampleCount,\n files,\n otherCount: fileOtherCount,\n });\n }\n\n // Sort packages by timeUs descending\n packages.sort((a, b) => b.timeUs - a.timeUs);\n\n return {\n timestamp: new Date().toLocaleString(),\n totalTimeUs,\n packages,\n otherCount: topLevelOtherCount,\n };\n}\n","/**\n * Format utilities for the HTML reporter.\n * Pure functions with defined input/output contracts.\n */\n\n/**\n * Convert microseconds to adaptive human-readable time string.\n *\n * - >= 1s: shows seconds with 2 decimal places (e.g. \"1.24s\")\n * - < 1s: shows rounded milliseconds (e.g. \"432ms\")\n * - Sub-millisecond values round up to 1ms (never shows \"0ms\" for nonzero input)\n * - Zero returns \"0ms\"\n */\nexport function formatTime(us: number): string {\n if (us === 0) return '0ms';\n\n const ms = us / 1000;\n\n if (ms >= 1000) {\n const seconds = ms / 1000;\n return `${seconds.toFixed(2)}s`;\n }\n\n const rounded = Math.round(ms);\n return `${rounded < 1 ? 1 : rounded}ms`;\n}\n\n/**\n * Convert microseconds to percentage of total with one decimal place.\n * Returns \"0.0%\" when totalUs is zero (avoids division by zero).\n */\nexport function formatPct(us: number, totalUs: number): string {\n if (totalUs === 0) return '0.0%';\n return `${((us / totalUs) * 100).toFixed(1)}%`;\n}\n\n/**\n * Escape HTML-special characters to prevent broken markup.\n * Handles: & < > \" '\n * Ampersand is replaced first to avoid double-escaping.\n */\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","/**\n * HTML renderer for the profiling report.\n *\n * Generates a self-contained HTML file (inline CSS, no external dependencies)\n * with a summary table and expandable Package > File > Function tree.\n */\n\nimport type { ReportData, PackageEntry, FileEntry } from '../types.js';\nimport { formatTime, formatPct, escapeHtml } from './format.js';\n\nfunction generateCss(): string {\n return `\n :root {\n --bg: #fafbfc;\n --text: #1a1a2e;\n --muted: #8b8fa3;\n --border: #e2e4ea;\n --first-party-accent: #3b6cf5;\n --first-party-bg: #eef2ff;\n --dep-bg: #ffffff;\n --bar-track: #e8eaed;\n --bar-fill: #5b8def;\n --bar-fill-fp: #3b6cf5;\n --other-text: #a0a4b8;\n --table-header-bg: #f4f5f7;\n --shadow: 0 1px 3px rgba(0,0,0,0.06);\n --radius: 6px;\n --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;\n --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n }\n\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n font-family: var(--font-sans);\n background: var(--bg);\n color: var(--text);\n line-height: 1.5;\n padding: 2rem;\n max-width: 960px;\n margin: 0 auto;\n }\n\n h1 {\n font-size: 1.5rem;\n font-weight: 600;\n margin-bottom: 0.25rem;\n }\n\n .meta {\n color: var(--muted);\n font-size: 0.85rem;\n margin-bottom: 2rem;\n }\n\n h2 {\n font-size: 1.1rem;\n font-weight: 600;\n margin-bottom: 0.75rem;\n margin-top: 2rem;\n }\n\n /* Summary table */\n table {\n width: 100%;\n border-collapse: collapse;\n background: #fff;\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n margin-bottom: 1rem;\n }\n\n th {\n text-align: left;\n background: var(--table-header-bg);\n padding: 0.6rem 0.75rem;\n font-size: 0.8rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--muted);\n border-bottom: 1px solid var(--border);\n }\n\n td {\n padding: 0.55rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.9rem;\n }\n\n tr:last-child td { border-bottom: none; }\n\n tr.first-party td:first-child {\n border-left: 3px solid var(--first-party-accent);\n padding-left: calc(0.75rem - 3px);\n }\n\n td.pkg-name { font-family: var(--font-mono); font-size: 0.85rem; }\n td.numeric { text-align: right; font-family: var(--font-mono); font-size: 0.85rem; }\n\n .bar-cell {\n width: 30%;\n padding-right: 1rem;\n }\n\n .bar-container {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n\n .bar-track {\n flex: 1;\n height: 8px;\n background: var(--bar-track);\n border-radius: 4px;\n overflow: hidden;\n }\n\n .bar-fill {\n height: 100%;\n border-radius: 4px;\n background: var(--bar-fill);\n min-width: 1px;\n }\n\n tr.first-party .bar-fill {\n background: var(--bar-fill-fp);\n }\n\n .bar-pct {\n font-family: var(--font-mono);\n font-size: 0.8rem;\n min-width: 3.5em;\n text-align: right;\n }\n\n tr.other-row td {\n color: var(--other-text);\n font-style: italic;\n }\n\n /* Tree */\n .tree {\n background: #fff;\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n }\n\n details {\n border-bottom: 1px solid var(--border);\n }\n\n details:last-child { border-bottom: none; }\n\n details details { border-bottom: 1px solid var(--border); }\n details details:last-child { border-bottom: none; }\n\n summary {\n cursor: pointer;\n list-style: none;\n padding: 0.6rem 0.75rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.9rem;\n user-select: none;\n }\n\n summary::-webkit-details-marker { display: none; }\n\n summary::before {\n content: '\\\\25B6';\n font-size: 0.6rem;\n color: var(--muted);\n transition: transform 0.15s ease;\n flex-shrink: 0;\n }\n\n details[open] > summary::before {\n transform: rotate(90deg);\n }\n\n .tree-name {\n font-family: var(--font-mono);\n font-size: 0.85rem;\n flex: 1;\n }\n\n .tree-label {\n font-family: var(--font-sans);\n font-size: 0.65rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n padding: 0.1rem 0.35rem;\n border-radius: 3px;\n flex-shrink: 0;\n }\n\n .tree-label.pkg { background: #e8eaed; color: #555; }\n .tree-label.file { background: #e8f0fe; color: #3b6cf5; }\n .tree-label.fn { background: #f0f0f0; color: #777; }\n\n .tree-stats {\n font-family: var(--font-mono);\n font-size: 0.8rem;\n color: var(--muted);\n flex-shrink: 0;\n }\n\n /* Level indentation */\n .level-0 > summary { padding-left: 0.75rem; }\n .level-1 > summary { padding-left: 2rem; }\n .level-2 { padding: 0.45rem 0.75rem 0.45rem 3.25rem; font-size: 0.85rem; display: flex; align-items: center; gap: 0.5rem; }\n\n /* First-party package highlight */\n .fp-pkg > summary {\n background: var(--first-party-bg);\n border-left: 3px solid var(--first-party-accent);\n }\n\n .other-item {\n padding: 0.45rem 0.75rem;\n color: var(--other-text);\n font-style: italic;\n font-size: 0.85rem;\n }\n\n .other-item.indent-1 { padding-left: 2rem; }\n .other-item.indent-2 { padding-left: 3.25rem; }\n\n @media (max-width: 600px) {\n body { padding: 1rem; }\n .bar-cell { width: 25%; }\n }\n `;\n}\n\nfunction renderSummaryTable(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n): string {\n let rows = '';\n\n for (const pkg of packages) {\n const cls = pkg.isFirstParty ? 'first-party' : 'dependency';\n const pctVal = totalTimeUs > 0 ? (pkg.timeUs / totalTimeUs) * 100 : 0;\n rows += `\n <tr class=\"${cls}\">\n <td class=\"pkg-name\">${escapeHtml(pkg.name)}</td>\n <td class=\"numeric\">${escapeHtml(formatTime(pkg.timeUs))}</td>\n <td class=\"bar-cell\">\n <div class=\"bar-container\">\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${pctVal.toFixed(1)}%\"></div></div>\n <span class=\"bar-pct\">${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))}</span>\n </div>\n </td>\n <td class=\"numeric\">${pkg.sampleCount}</td>\n </tr>`;\n }\n\n if (otherCount > 0) {\n rows += `\n <tr class=\"other-row\">\n <td class=\"pkg-name\">Other (${otherCount} items)</td>\n <td class=\"numeric\"></td>\n <td class=\"bar-cell\"></td>\n <td class=\"numeric\"></td>\n </tr>`;\n }\n\n return `\n <table>\n <thead>\n <tr>\n <th>Package</th>\n <th>Wall Time</th>\n <th>% of Total</th>\n <th>Samples</th>\n </tr>\n </thead>\n <tbody>${rows}\n </tbody>\n </table>`;\n}\n\nfunction renderTree(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n): string {\n let html = '<div class=\"tree\">';\n\n for (const pkg of packages) {\n const fpCls = pkg.isFirstParty ? ' fp-pkg' : '';\n html += `<details class=\"level-0${fpCls}\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label pkg\">pkg</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(pkg.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;\n html += `</summary>`;\n\n for (const file of pkg.files) {\n html += `<details class=\"level-1\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label file\">file</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(file.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(file.timeUs))} · ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} · ${file.sampleCount} samples</span>`;\n html += `</summary>`;\n\n for (const fn of file.functions) {\n html += `<div class=\"level-2\">`;\n html += `<span class=\"tree-label fn\">fn</span> `;\n html += `<span class=\"tree-name\">${escapeHtml(fn.name)}</span>`;\n html += ` <span class=\"tree-stats\">${escapeHtml(formatTime(fn.timeUs))} · ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} · ${fn.sampleCount} samples</span>`;\n html += `</div>`;\n }\n\n if (file.otherCount > 0) {\n html += `<div class=\"other-item indent-2\">Other (${file.otherCount} items)</div>`;\n }\n\n html += `</details>`;\n }\n\n if (pkg.otherCount > 0) {\n html += `<div class=\"other-item indent-1\">Other (${pkg.otherCount} items)</div>`;\n }\n\n html += `</details>`;\n }\n\n if (otherCount > 0) {\n html += `<div class=\"other-item\">Other (${otherCount} packages)</div>`;\n }\n\n html += '</div>';\n return html;\n}\n\n/**\n * Render a complete self-contained HTML report from aggregated profiling data.\n */\nexport function renderHtml(data: ReportData): string {\n const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs);\n const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs);\n const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>where-you-at report</title>\n <style>${generateCss()}\n </style>\n</head>\n<body>\n <h1>where-you-at</h1>\n <div class=\"meta\">Generated ${escapeHtml(data.timestamp)} · Total wall time: ${totalFormatted}</div>\n\n <h2>Summary</h2>\n ${summaryTable}\n\n <h2>Details</h2>\n ${tree}\n</body>\n</html>`;\n}\n","/**\n * Reporter orchestrator.\n *\n * Aggregates SampleStore data, renders HTML, writes file to cwd,\n * and returns the file path.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { SampleStore } from './sample-store.js';\nimport { aggregate } from './reporter/aggregate.js';\nimport { renderHtml } from './reporter/html.js';\n\nfunction generateFilename(): string {\n const now = new Date();\n const pad = (n: number) => String(n).padStart(2, '0');\n const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;\n const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;\n return `where-you-at-${date}-${time}.html`;\n}\n\nfunction readProjectName(cwd: string): string {\n try {\n const raw = readFileSync(join(cwd, 'package.json'), 'utf-8');\n const pkg = JSON.parse(raw) as { name?: string };\n return pkg.name ?? 'app';\n } catch {\n return 'app';\n }\n}\n\nexport function generateReport(store: SampleStore, cwd?: string): string {\n const resolvedCwd = cwd ?? process.cwd();\n const projectName = readProjectName(resolvedCwd);\n const data = aggregate(store, projectName);\n const html = renderHtml(data);\n const filename = generateFilename();\n const filepath = join(resolvedCwd, filename);\n writeFileSync(filepath, html, 'utf-8');\n console.log(`Report written to ./${filename}`);\n return filepath;\n}\n","/**\n * Accumulates per-package wall time (microseconds) from the V8 CPU profiler.\n *\n * Data structure: nested Maps -- package -> file -> function -> microseconds.\n * This naturally matches the package-first tree output that the reporter\n * needs in Phase 3. O(1) lookups at each level, no serialization overhead.\n *\n * A parallel sampleCounts structure tracks raw sample counts (incremented by 1\n * per record() call) for the summary table's \"Sample count\" column.\n */\nexport class SampleStore {\n private data = new Map<string, Map<string, Map<string, number>>>();\n private counts = new Map<string, Map<string, Map<string, number>>>();\n private internalCount = 0;\n private internalSamples = 0;\n\n /**\n * Record a sample for a user-code frame.\n * Accumulates deltaUs microseconds for the given (package, file, function) triple,\n * and increments the parallel sample count by 1.\n */\n record(packageName: string, relativePath: string, functionId: string, deltaUs: number): void {\n // Accumulate microseconds\n let fileMap = this.data.get(packageName);\n if (fileMap === undefined) {\n fileMap = new Map<string, Map<string, number>>();\n this.data.set(packageName, fileMap);\n }\n\n let funcMap = fileMap.get(relativePath);\n if (funcMap === undefined) {\n funcMap = new Map<string, number>();\n fileMap.set(relativePath, funcMap);\n }\n\n funcMap.set(functionId, (funcMap.get(functionId) ?? 0) + deltaUs);\n\n // Parallel sample count (always +1)\n let countFileMap = this.counts.get(packageName);\n if (countFileMap === undefined) {\n countFileMap = new Map<string, Map<string, number>>();\n this.counts.set(packageName, countFileMap);\n }\n\n let countFuncMap = countFileMap.get(relativePath);\n if (countFuncMap === undefined) {\n countFuncMap = new Map<string, number>();\n countFileMap.set(relativePath, countFuncMap);\n }\n\n countFuncMap.set(functionId, (countFuncMap.get(functionId) ?? 0) + 1);\n }\n\n /** Record an internal/filtered frame (empty URL, eval, wasm, idle, etc). */\n recordInternal(deltaUs: number): void {\n this.internalCount += deltaUs;\n this.internalSamples += 1;\n }\n\n /** Reset all accumulated data to a clean state. */\n clear(): void {\n this.data = new Map<string, Map<string, Map<string, number>>>();\n this.counts = new Map<string, Map<string, Map<string, number>>>();\n this.internalCount = 0;\n this.internalSamples = 0;\n }\n\n /** Read-only access to the accumulated sample data (microseconds). */\n get packages(): ReadonlyMap<string, Map<string, Map<string, number>>> {\n return this.data;\n }\n\n /** Count of internal/filtered microseconds recorded. */\n get internal(): number {\n return this.internalCount;\n }\n\n /** Read-only access to the parallel sample counts. */\n get sampleCountsByPackage(): ReadonlyMap<string, Map<string, Map<string, number>>> {\n return this.counts;\n }\n\n /** Count of internal/filtered samples (raw count, not microseconds). */\n get internalSampleCount(): number {\n return this.internalSamples;\n }\n}\n","import { Session } from 'node:inspector/promises';\nimport type { Profiler } from 'node:inspector';\nimport { parseFrame } from './frame-parser.js';\nimport { PackageResolver } from './package-resolver.js';\nimport { generateReport } from './reporter.js';\nimport { SampleStore } from './sample-store.js';\nimport type { RawCallFrame } from './types.js';\n\n// Module-level state -- lazy initialization\nlet session: Session | null = null;\nlet profiling = false;\nconst store = new SampleStore();\nconst resolver = new PackageResolver(process.cwd());\n\n/**\n * Start the V8 CPU profiler. If already profiling, this is a safe no-op.\n */\nexport async function track(options?: { interval?: number }): Promise<void> {\n if (profiling) return;\n\n if (session === null) {\n session = new Session();\n session.connect();\n }\n\n await session.post('Profiler.enable');\n\n if (options?.interval !== undefined) {\n await session.post('Profiler.setSamplingInterval', {\n interval: options.interval,\n });\n }\n\n await session.post('Profiler.start');\n profiling = true;\n}\n\n/**\n * Stop the profiler (if running) and reset all accumulated sample data.\n */\nexport async function clear(): Promise<void> {\n if (profiling && session) {\n await session.post('Profiler.stop');\n await session.post('Profiler.disable');\n profiling = false;\n }\n store.clear();\n}\n\n/**\n * Stop the profiler, process collected samples through the data pipeline\n * (parseFrame -> PackageResolver -> SampleStore), generate an HTML report,\n * and return the file path. Resets the store after reporting (clean slate\n * for next cycle).\n *\n * Returns the absolute path to the generated HTML file, or empty string\n * if no samples were collected.\n */\nexport async function report(): Promise<string> {\n if (!profiling || !session) {\n console.log('no samples collected');\n return '';\n }\n\n const { profile } = await session.post('Profiler.stop');\n await session.post('Profiler.disable');\n profiling = false;\n\n processProfile(profile);\n\n let filepath = '';\n\n if (store.packages.size > 0) {\n filepath = generateReport(store);\n } else {\n console.log('no samples collected');\n }\n\n store.clear();\n return filepath;\n}\n\n/**\n * Process a V8 CPUProfile: walk each sample, parse the frame, resolve\n * the package, and record into the store. Uses timeDeltas for wall-time\n * microsecond accumulation.\n */\nfunction processProfile(profile: Profiler.Profile): void {\n const nodeMap = new Map(profile.nodes.map((n) => [n.id, n]));\n const samples = profile.samples ?? [];\n const timeDeltas = profile.timeDeltas ?? [];\n\n for (let i = 0; i < samples.length; i++) {\n const node = nodeMap.get(samples[i]!);\n if (!node) continue;\n\n const deltaUs = timeDeltas[i] ?? 0;\n const parsed = parseFrame(node.callFrame as RawCallFrame);\n\n if (parsed.kind === 'user') {\n if (parsed.filePath.startsWith('node:')) {\n // Node.js built-in: attribute to \"node (built-in)\" package\n const relativePath = parsed.filePath.slice(5);\n store.record('node (built-in)', relativePath, parsed.functionId, deltaUs);\n } else {\n const { packageName, relativePath } = resolver.resolve(parsed.filePath);\n store.record(packageName, relativePath, parsed.functionId, deltaUs);\n }\n } else {\n store.recordInternal(deltaUs);\n }\n }\n}\n\n/** @internal -- exposed for testing only */\nexport function _getStore(): SampleStore {\n return store;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["sep","Session"],"sources":["../src/frame-parser.ts","../src/package-resolver.ts","../src/reporter/aggregate.ts","../src/reporter/format.ts","../src/reporter/html.ts","../src/reporter.ts","../src/sample-store.ts","../src/sampler.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport type { RawCallFrame, ParsedFrame } from './types.js';\n\n/**\n * Classify a V8 CPU profiler call frame and convert its URL to a filesystem path.\n *\n * Every sampled frame from the V8 profiler passes through this function first.\n * It determines the frame kind (user code, internal, eval, wasm) and for user\n * frames converts the URL to a filesystem path and builds a human-readable\n * function identifier.\n */\nexport function parseFrame(frame: RawCallFrame): ParsedFrame {\n const { url, functionName, lineNumber } = frame;\n\n // Empty URL: V8 internal pseudo-frames like (idle), (root), (gc), (program)\n if (url === '') {\n return { kind: 'internal' };\n }\n\n // Node.js built-in modules -- treated as attributable user frames\n if (url.startsWith('node:')) {\n const functionId = `${functionName || '<anonymous>'}:${lineNumber + 1}`;\n return { kind: 'user', filePath: url, functionId };\n }\n\n // WebAssembly frames\n if (url.startsWith('wasm:')) {\n return { kind: 'wasm' };\n }\n\n // Eval frames\n if (url.includes('eval')) {\n return { kind: 'eval' };\n }\n\n // User code: convert URL to filesystem path\n const filePath = url.startsWith('file://')\n ? fileURLToPath(url)\n : url;\n\n // Build human-readable function identifier (convert 0-based to 1-based line)\n const functionId = `${functionName || '<anonymous>'}:${lineNumber + 1}`;\n\n return { kind: 'user', filePath, functionId };\n}\n","import { readFileSync } from 'node:fs';\nimport { dirname, join, relative, sep } from 'node:path';\n\n/**\n * Resolves an absolute file path to a package name and relative path.\n *\n * For node_modules paths: extracts the package name from the last /node_modules/\n * segment (critical for pnpm virtual store compatibility). Handles scoped packages.\n *\n * For first-party files: walks up directory tree looking for package.json,\n * falls back to 'app' if none found.\n */\nexport class PackageResolver {\n private readonly projectRoot: string;\n private readonly packageJsonCache = new Map<string, string | null>();\n\n constructor(projectRoot: string) {\n this.projectRoot = projectRoot;\n }\n\n resolve(absoluteFilePath: string): { packageName: string; relativePath: string } {\n const nodeModulesSeg = `${sep}node_modules${sep}`;\n const lastIdx = absoluteFilePath.lastIndexOf(nodeModulesSeg);\n\n if (lastIdx !== -1) {\n // node_modules path -- extract package name from LAST /node_modules/ segment\n const afterModules = absoluteFilePath.substring(lastIdx + nodeModulesSeg.length);\n const segments = afterModules.split(sep);\n\n let packageName: string;\n let fileStartIdx: number;\n\n if (segments[0]!.startsWith('@')) {\n // Scoped package: @scope/name\n packageName = `${segments[0]}/${segments[1]}`;\n fileStartIdx = 2;\n } else {\n packageName = segments[0]!;\n fileStartIdx = 1;\n }\n\n const relativePath = segments.slice(fileStartIdx).join('/');\n\n return { packageName, relativePath };\n }\n\n // First-party file -- walk up looking for package.json\n const packageName = this.findPackageName(absoluteFilePath);\n const relativePath = relative(this.projectRoot, absoluteFilePath)\n .split(sep)\n .join('/');\n\n return { packageName, relativePath };\n }\n\n /**\n * Walk up from the file's directory looking for package.json.\n * Cache results to avoid repeated filesystem reads.\n */\n private findPackageName(absoluteFilePath: string): string {\n let dir = dirname(absoluteFilePath);\n\n while (true) {\n const cached = this.packageJsonCache.get(dir);\n if (cached !== undefined) {\n if (cached !== null) {\n return cached;\n }\n // null means we checked this dir and no package.json -- continue up\n } else {\n try {\n const raw = readFileSync(join(dir, 'package.json'), 'utf-8');\n const pkg = JSON.parse(raw) as { name?: string };\n const name = pkg.name ?? null;\n this.packageJsonCache.set(dir, name);\n if (name !== null) {\n return name;\n }\n } catch {\n // No package.json here -- cache as null and continue\n this.packageJsonCache.set(dir, null);\n }\n }\n\n const parent = dirname(dir);\n if (parent === dir) {\n // Reached filesystem root without finding a named package.json\n return 'app';\n }\n dir = parent;\n }\n }\n}\n","/**\n * Transform raw SampleStore data into a sorted, thresholded ReportData structure.\n *\n * Pure function: reads nested Maps from SampleStore, applies a 5% threshold\n * at every level (package, file, function) relative to the total profiled time,\n * aggregates below-threshold entries into an \"other\" count, and sorts by\n * timeUs descending at each level.\n */\n\nimport type { SampleStore } from '../sample-store.js';\nimport type {\n ReportData,\n PackageEntry,\n FileEntry,\n FunctionEntry,\n} from '../types.js';\n\nconst THRESHOLD_PCT = 0.05;\n\n/**\n * Aggregate SampleStore data into a ReportData structure.\n *\n * @param store - SampleStore with accumulated microseconds and sample counts\n * @param projectName - Name of the first-party project (for isFirstParty flag)\n * @returns ReportData with packages sorted desc by time, thresholded at 5%\n */\nexport function aggregate(store: SampleStore, projectName: string): ReportData {\n // 1. Calculate total user-attributed time\n let totalTimeUs = 0;\n for (const fileMap of store.packages.values()) {\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n totalTimeUs += us;\n }\n }\n }\n\n if (totalTimeUs === 0) {\n return {\n timestamp: new Date().toLocaleString(),\n totalTimeUs: 0,\n packages: [],\n otherCount: 0,\n projectName,\n };\n }\n\n const threshold = totalTimeUs * THRESHOLD_PCT;\n const packages: PackageEntry[] = [];\n let topLevelOtherCount = 0;\n\n // 2. Process each package\n for (const [packageName, fileMap] of store.packages) {\n // Sum total time for this package\n let packageTimeUs = 0;\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n packageTimeUs += us;\n }\n }\n\n // Apply threshold at package level\n if (packageTimeUs < threshold) {\n topLevelOtherCount++;\n continue;\n }\n\n // Sum total sample count for this package\n let packageSampleCount = 0;\n const countFileMap = store.sampleCountsByPackage.get(packageName);\n if (countFileMap) {\n for (const countFuncMap of countFileMap.values()) {\n for (const count of countFuncMap.values()) {\n packageSampleCount += count;\n }\n }\n }\n\n // 3. Process files within the package\n const files: FileEntry[] = [];\n let fileOtherCount = 0;\n\n for (const [fileName, funcMap] of fileMap) {\n // Sum time for this file\n let fileTimeUs = 0;\n for (const us of funcMap.values()) {\n fileTimeUs += us;\n }\n\n // Apply threshold at file level (relative to total)\n if (fileTimeUs < threshold) {\n fileOtherCount++;\n continue;\n }\n\n // Sum sample count for this file\n let fileSampleCount = 0;\n const countFuncMap = countFileMap?.get(fileName);\n if (countFuncMap) {\n for (const count of countFuncMap.values()) {\n fileSampleCount += count;\n }\n }\n\n // 4. Process functions within the file\n const functions: FunctionEntry[] = [];\n let funcOtherCount = 0;\n\n for (const [funcName, funcTimeUs] of funcMap) {\n // Apply threshold at function level (relative to total)\n if (funcTimeUs < threshold) {\n funcOtherCount++;\n continue;\n }\n\n const funcSampleCount = countFuncMap?.get(funcName) ?? 0;\n\n functions.push({\n name: funcName,\n timeUs: funcTimeUs,\n pct: (funcTimeUs / totalTimeUs) * 100,\n sampleCount: funcSampleCount,\n });\n }\n\n // Sort functions by timeUs descending\n functions.sort((a, b) => b.timeUs - a.timeUs);\n\n files.push({\n name: fileName,\n timeUs: fileTimeUs,\n pct: (fileTimeUs / totalTimeUs) * 100,\n sampleCount: fileSampleCount,\n functions,\n otherCount: funcOtherCount,\n });\n }\n\n // Sort files by timeUs descending\n files.sort((a, b) => b.timeUs - a.timeUs);\n\n packages.push({\n name: packageName,\n timeUs: packageTimeUs,\n pct: (packageTimeUs / totalTimeUs) * 100,\n isFirstParty: packageName === projectName,\n sampleCount: packageSampleCount,\n files,\n otherCount: fileOtherCount,\n });\n }\n\n // Sort packages by timeUs descending\n packages.sort((a, b) => b.timeUs - a.timeUs);\n\n return {\n timestamp: new Date().toLocaleString(),\n totalTimeUs,\n packages,\n otherCount: topLevelOtherCount,\n projectName,\n };\n}\n","/**\n * Format utilities for the HTML reporter.\n * Pure functions with defined input/output contracts.\n */\n\n/**\n * Convert microseconds to adaptive human-readable time string.\n *\n * - >= 1s: shows seconds with 2 decimal places (e.g. \"1.24s\")\n * - < 1s: shows rounded milliseconds (e.g. \"432ms\")\n * - Sub-millisecond values round up to 1ms (never shows \"0ms\" for nonzero input)\n * - Zero returns \"0ms\"\n */\nexport function formatTime(us: number): string {\n if (us === 0) return '0ms';\n\n const ms = us / 1000;\n\n if (ms >= 1000) {\n const seconds = ms / 1000;\n return `${seconds.toFixed(2)}s`;\n }\n\n const rounded = Math.round(ms);\n return `${rounded < 1 ? 1 : rounded}ms`;\n}\n\n/**\n * Convert microseconds to percentage of total with one decimal place.\n * Returns \"0.0%\" when totalUs is zero (avoids division by zero).\n */\nexport function formatPct(us: number, totalUs: number): string {\n if (totalUs === 0) return '0.0%';\n return `${((us / totalUs) * 100).toFixed(1)}%`;\n}\n\n/**\n * Escape HTML-special characters to prevent broken markup.\n * Handles: & < > \" '\n * Ampersand is replaced first to avoid double-escaping.\n */\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","/**\n * HTML renderer for the profiling report.\n *\n * Generates a self-contained HTML file (inline CSS, no external dependencies)\n * with a summary table and expandable Package > File > Function tree.\n */\n\nimport type { ReportData, PackageEntry, FileEntry } from '../types.js';\nimport { formatTime, formatPct, escapeHtml } from './format.js';\n\nfunction generateCss(): string {\n return `\n :root {\n --bg: #fafbfc;\n --text: #1a1a2e;\n --muted: #8b8fa3;\n --border: #e2e4ea;\n --first-party-accent: #3b6cf5;\n --first-party-bg: #eef2ff;\n --dep-bg: #ffffff;\n --bar-track: #e8eaed;\n --bar-fill: #5b8def;\n --bar-fill-fp: #3b6cf5;\n --other-text: #a0a4b8;\n --table-header-bg: #f4f5f7;\n --shadow: 0 1px 3px rgba(0,0,0,0.06);\n --radius: 6px;\n --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;\n --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n }\n\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n font-family: var(--font-sans);\n background: var(--bg);\n color: var(--text);\n line-height: 1.5;\n padding: 2rem;\n max-width: 960px;\n margin: 0 auto;\n }\n\n h1 {\n font-size: 1.5rem;\n font-weight: 600;\n margin-bottom: 0.25rem;\n }\n\n .meta {\n color: var(--muted);\n font-size: 0.85rem;\n margin-bottom: 2rem;\n }\n\n h2 {\n font-size: 1.1rem;\n font-weight: 600;\n margin-bottom: 0.75rem;\n margin-top: 2rem;\n }\n\n /* Summary table */\n table {\n width: 100%;\n border-collapse: collapse;\n background: #fff;\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n margin-bottom: 1rem;\n }\n\n th {\n text-align: left;\n background: var(--table-header-bg);\n padding: 0.6rem 0.75rem;\n font-size: 0.8rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--muted);\n border-bottom: 1px solid var(--border);\n }\n\n td {\n padding: 0.55rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.9rem;\n }\n\n tr:last-child td { border-bottom: none; }\n\n tr.first-party td:first-child {\n border-left: 3px solid var(--first-party-accent);\n padding-left: calc(0.75rem - 3px);\n }\n\n td.pkg-name { font-family: var(--font-mono); font-size: 0.85rem; }\n td.numeric { text-align: right; font-family: var(--font-mono); font-size: 0.85rem; }\n\n .bar-cell {\n width: 30%;\n padding-right: 1rem;\n }\n\n .bar-container {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n\n .bar-track {\n flex: 1;\n height: 8px;\n background: var(--bar-track);\n border-radius: 4px;\n overflow: hidden;\n }\n\n .bar-fill {\n height: 100%;\n border-radius: 4px;\n background: var(--bar-fill);\n min-width: 1px;\n }\n\n tr.first-party .bar-fill {\n background: var(--bar-fill-fp);\n }\n\n .bar-pct {\n font-family: var(--font-mono);\n font-size: 0.8rem;\n min-width: 3.5em;\n text-align: right;\n }\n\n tr.other-row td {\n color: var(--other-text);\n font-style: italic;\n }\n\n /* Tree */\n .tree {\n background: #fff;\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n }\n\n details {\n border-bottom: 1px solid var(--border);\n }\n\n details:last-child { border-bottom: none; }\n\n details details { border-bottom: 1px solid var(--border); }\n details details:last-child { border-bottom: none; }\n\n summary {\n cursor: pointer;\n list-style: none;\n padding: 0.6rem 0.75rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.9rem;\n user-select: none;\n }\n\n summary::-webkit-details-marker { display: none; }\n\n summary::before {\n content: '\\\\25B6';\n font-size: 0.6rem;\n color: var(--muted);\n transition: transform 0.15s ease;\n flex-shrink: 0;\n }\n\n details[open] > summary::before {\n transform: rotate(90deg);\n }\n\n .tree-name {\n font-family: var(--font-mono);\n font-size: 0.85rem;\n flex: 1;\n }\n\n .tree-label {\n font-family: var(--font-sans);\n font-size: 0.65rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n padding: 0.1rem 0.35rem;\n border-radius: 3px;\n flex-shrink: 0;\n }\n\n .tree-label.pkg { background: #e8eaed; color: #555; }\n .tree-label.file { background: #e8f0fe; color: #3b6cf5; }\n .tree-label.fn { background: #f0f0f0; color: #777; }\n\n .tree-stats {\n font-family: var(--font-mono);\n font-size: 0.8rem;\n color: var(--muted);\n flex-shrink: 0;\n }\n\n /* Level indentation */\n .level-0 > summary { padding-left: 0.75rem; }\n .level-1 > summary { padding-left: 2rem; }\n .level-2 { padding: 0.45rem 0.75rem 0.45rem 3.25rem; font-size: 0.85rem; display: flex; align-items: center; gap: 0.5rem; }\n\n /* First-party package highlight */\n .fp-pkg > summary {\n background: var(--first-party-bg);\n border-left: 3px solid var(--first-party-accent);\n }\n\n .other-item {\n padding: 0.45rem 0.75rem;\n color: var(--other-text);\n font-style: italic;\n font-size: 0.85rem;\n }\n\n .other-item.indent-1 { padding-left: 2rem; }\n .other-item.indent-2 { padding-left: 3.25rem; }\n\n @media (max-width: 600px) {\n body { padding: 1rem; }\n .bar-cell { width: 25%; }\n }\n `;\n}\n\nfunction renderSummaryTable(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n): string {\n let rows = '';\n\n for (const pkg of packages) {\n const cls = pkg.isFirstParty ? 'first-party' : 'dependency';\n const pctVal = totalTimeUs > 0 ? (pkg.timeUs / totalTimeUs) * 100 : 0;\n rows += `\n <tr class=\"${cls}\">\n <td class=\"pkg-name\">${escapeHtml(pkg.name)}</td>\n <td class=\"numeric\">${escapeHtml(formatTime(pkg.timeUs))}</td>\n <td class=\"bar-cell\">\n <div class=\"bar-container\">\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${pctVal.toFixed(1)}%\"></div></div>\n <span class=\"bar-pct\">${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))}</span>\n </div>\n </td>\n <td class=\"numeric\">${pkg.sampleCount}</td>\n </tr>`;\n }\n\n if (otherCount > 0) {\n rows += `\n <tr class=\"other-row\">\n <td class=\"pkg-name\">Other (${otherCount} items)</td>\n <td class=\"numeric\"></td>\n <td class=\"bar-cell\"></td>\n <td class=\"numeric\"></td>\n </tr>`;\n }\n\n return `\n <table>\n <thead>\n <tr>\n <th>Package</th>\n <th>Wall Time</th>\n <th>% of Total</th>\n <th>Samples</th>\n </tr>\n </thead>\n <tbody>${rows}\n </tbody>\n </table>`;\n}\n\nfunction renderTree(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n): string {\n let html = '<div class=\"tree\">';\n\n for (const pkg of packages) {\n const fpCls = pkg.isFirstParty ? ' fp-pkg' : '';\n html += `<details class=\"level-0${fpCls}\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label pkg\">pkg</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(pkg.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;\n html += `</summary>`;\n\n for (const file of pkg.files) {\n html += `<details class=\"level-1\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label file\">file</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(file.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(file.timeUs))} · ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} · ${file.sampleCount} samples</span>`;\n html += `</summary>`;\n\n for (const fn of file.functions) {\n html += `<div class=\"level-2\">`;\n html += `<span class=\"tree-label fn\">fn</span> `;\n html += `<span class=\"tree-name\">${escapeHtml(fn.name)}</span>`;\n html += ` <span class=\"tree-stats\">${escapeHtml(formatTime(fn.timeUs))} · ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} · ${fn.sampleCount} samples</span>`;\n html += `</div>`;\n }\n\n if (file.otherCount > 0) {\n html += `<div class=\"other-item indent-2\">Other (${file.otherCount} items)</div>`;\n }\n\n html += `</details>`;\n }\n\n if (pkg.otherCount > 0) {\n html += `<div class=\"other-item indent-1\">Other (${pkg.otherCount} items)</div>`;\n }\n\n html += `</details>`;\n }\n\n if (otherCount > 0) {\n html += `<div class=\"other-item\">Other (${otherCount} packages)</div>`;\n }\n\n html += '</div>';\n return html;\n}\n\n/**\n * Render a complete self-contained HTML report from aggregated profiling data.\n */\nexport function renderHtml(data: ReportData): string {\n const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs);\n const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs);\n const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));\n\n const titleName = escapeHtml(data.projectName);\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>${titleName} · where-you-at report</title>\n <style>${generateCss()}\n </style>\n</head>\n<body>\n <h1>${titleName}</h1>\n <div class=\"meta\">Generated ${escapeHtml(data.timestamp)} · Total wall time: ${totalFormatted}</div>\n\n <h2>Summary</h2>\n ${summaryTable}\n\n <h2>Details</h2>\n ${tree}\n</body>\n</html>`;\n}\n","/**\n * Reporter orchestrator.\n *\n * Aggregates SampleStore data, renders HTML, writes file to cwd,\n * and returns the file path.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { SampleStore } from './sample-store.js';\nimport { aggregate } from './reporter/aggregate.js';\nimport { renderHtml } from './reporter/html.js';\n\nfunction generateFilename(): string {\n const now = new Date();\n const pad = (n: number) => String(n).padStart(2, '0');\n const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;\n const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;\n return `where-you-at-${date}-${time}.html`;\n}\n\nfunction readProjectName(cwd: string): string {\n try {\n const raw = readFileSync(join(cwd, 'package.json'), 'utf-8');\n const pkg = JSON.parse(raw) as { name?: string };\n return pkg.name ?? 'app';\n } catch {\n return 'app';\n }\n}\n\nexport function generateReport(store: SampleStore, cwd?: string): string {\n const resolvedCwd = cwd ?? process.cwd();\n const projectName = readProjectName(resolvedCwd);\n const data = aggregate(store, projectName);\n const html = renderHtml(data);\n const filename = generateFilename();\n const filepath = join(resolvedCwd, filename);\n writeFileSync(filepath, html, 'utf-8');\n console.log(`Report written to ./${filename}`);\n return filepath;\n}\n","/**\n * Accumulates per-package wall time (microseconds) from the V8 CPU profiler.\n *\n * Data structure: nested Maps -- package -> file -> function -> microseconds.\n * This naturally matches the package-first tree output that the reporter\n * needs in Phase 3. O(1) lookups at each level, no serialization overhead.\n *\n * A parallel sampleCounts structure tracks raw sample counts (incremented by 1\n * per record() call) for the summary table's \"Sample count\" column.\n */\nexport class SampleStore {\n private data = new Map<string, Map<string, Map<string, number>>>();\n private counts = new Map<string, Map<string, Map<string, number>>>();\n private internalCount = 0;\n private internalSamples = 0;\n\n /**\n * Record a sample for a user-code frame.\n * Accumulates deltaUs microseconds for the given (package, file, function) triple,\n * and increments the parallel sample count by 1.\n */\n record(packageName: string, relativePath: string, functionId: string, deltaUs: number): void {\n // Accumulate microseconds\n let fileMap = this.data.get(packageName);\n if (fileMap === undefined) {\n fileMap = new Map<string, Map<string, number>>();\n this.data.set(packageName, fileMap);\n }\n\n let funcMap = fileMap.get(relativePath);\n if (funcMap === undefined) {\n funcMap = new Map<string, number>();\n fileMap.set(relativePath, funcMap);\n }\n\n funcMap.set(functionId, (funcMap.get(functionId) ?? 0) + deltaUs);\n\n // Parallel sample count (always +1)\n let countFileMap = this.counts.get(packageName);\n if (countFileMap === undefined) {\n countFileMap = new Map<string, Map<string, number>>();\n this.counts.set(packageName, countFileMap);\n }\n\n let countFuncMap = countFileMap.get(relativePath);\n if (countFuncMap === undefined) {\n countFuncMap = new Map<string, number>();\n countFileMap.set(relativePath, countFuncMap);\n }\n\n countFuncMap.set(functionId, (countFuncMap.get(functionId) ?? 0) + 1);\n }\n\n /** Record an internal/filtered frame (empty URL, eval, wasm, idle, etc). */\n recordInternal(deltaUs: number): void {\n this.internalCount += deltaUs;\n this.internalSamples += 1;\n }\n\n /** Reset all accumulated data to a clean state. */\n clear(): void {\n this.data = new Map<string, Map<string, Map<string, number>>>();\n this.counts = new Map<string, Map<string, Map<string, number>>>();\n this.internalCount = 0;\n this.internalSamples = 0;\n }\n\n /** Read-only access to the accumulated sample data (microseconds). */\n get packages(): ReadonlyMap<string, Map<string, Map<string, number>>> {\n return this.data;\n }\n\n /** Count of internal/filtered microseconds recorded. */\n get internal(): number {\n return this.internalCount;\n }\n\n /** Read-only access to the parallel sample counts. */\n get sampleCountsByPackage(): ReadonlyMap<string, Map<string, Map<string, number>>> {\n return this.counts;\n }\n\n /** Count of internal/filtered samples (raw count, not microseconds). */\n get internalSampleCount(): number {\n return this.internalSamples;\n }\n}\n","import { Session } from 'node:inspector/promises';\nimport type { Profiler } from 'node:inspector';\nimport { parseFrame } from './frame-parser.js';\nimport { PackageResolver } from './package-resolver.js';\nimport { generateReport } from './reporter.js';\nimport { SampleStore } from './sample-store.js';\nimport type { RawCallFrame } from './types.js';\n\n// Module-level state -- lazy initialization\nlet session: Session | null = null;\nlet profiling = false;\nconst store = new SampleStore();\nconst resolver = new PackageResolver(process.cwd());\n\n/**\n * Start the V8 CPU profiler. If already profiling, this is a safe no-op.\n */\nexport async function track(options?: { interval?: number }): Promise<void> {\n if (profiling) return;\n\n if (session === null) {\n session = new Session();\n session.connect();\n }\n\n await session.post('Profiler.enable');\n\n if (options?.interval !== undefined) {\n await session.post('Profiler.setSamplingInterval', {\n interval: options.interval,\n });\n }\n\n await session.post('Profiler.start');\n profiling = true;\n}\n\n/**\n * Stop the profiler (if running) and reset all accumulated sample data.\n */\nexport async function clear(): Promise<void> {\n if (profiling && session) {\n await session.post('Profiler.stop');\n await session.post('Profiler.disable');\n profiling = false;\n }\n store.clear();\n}\n\n/**\n * Stop the profiler, process collected samples through the data pipeline\n * (parseFrame -> PackageResolver -> SampleStore), generate an HTML report,\n * and return the file path. Resets the store after reporting (clean slate\n * for next cycle).\n *\n * Returns the absolute path to the generated HTML file, or empty string\n * if no samples were collected.\n */\nexport async function report(): Promise<string> {\n if (!profiling || !session) {\n console.log('no samples collected');\n return '';\n }\n\n const { profile } = await session.post('Profiler.stop');\n await session.post('Profiler.disable');\n profiling = false;\n\n processProfile(profile);\n\n let filepath = '';\n\n if (store.packages.size > 0) {\n filepath = generateReport(store);\n } else {\n console.log('no samples collected');\n }\n\n store.clear();\n return filepath;\n}\n\n/**\n * Process a V8 CPUProfile: walk each sample, parse the frame, resolve\n * the package, and record into the store. Uses timeDeltas for wall-time\n * microsecond accumulation.\n */\nfunction processProfile(profile: Profiler.Profile): void {\n const nodeMap = new Map(profile.nodes.map((n) => [n.id, n]));\n const samples = profile.samples ?? [];\n const timeDeltas = profile.timeDeltas ?? [];\n\n for (let i = 0; i < samples.length; i++) {\n const node = nodeMap.get(samples[i]!);\n if (!node) continue;\n\n const deltaUs = timeDeltas[i] ?? 0;\n const parsed = parseFrame(node.callFrame as RawCallFrame);\n\n if (parsed.kind === 'user') {\n if (parsed.filePath.startsWith('node:')) {\n // Node.js built-in: attribute to \"node (built-in)\" package\n const relativePath = parsed.filePath.slice(5);\n store.record('node (built-in)', relativePath, parsed.functionId, deltaUs);\n } else {\n const { packageName, relativePath } = resolver.resolve(parsed.filePath);\n store.record(packageName, relativePath, parsed.functionId, deltaUs);\n }\n } else {\n store.recordInternal(deltaUs);\n }\n }\n}\n\n/** @internal -- exposed for testing only */\nexport function _getStore(): SampleStore {\n return store;\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -113,7 +113,8 @@ function aggregate(store, projectName) {
|
|
|
113
113
|
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
114
114
|
totalTimeUs: 0,
|
|
115
115
|
packages: [],
|
|
116
|
-
otherCount: 0
|
|
116
|
+
otherCount: 0,
|
|
117
|
+
projectName
|
|
117
118
|
};
|
|
118
119
|
const threshold = totalTimeUs * THRESHOLD_PCT;
|
|
119
120
|
const packages = [];
|
|
@@ -181,7 +182,8 @@ function aggregate(store, projectName) {
|
|
|
181
182
|
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
182
183
|
totalTimeUs,
|
|
183
184
|
packages,
|
|
184
|
-
otherCount: topLevelOtherCount
|
|
185
|
+
otherCount: topLevelOtherCount,
|
|
186
|
+
projectName
|
|
185
187
|
};
|
|
186
188
|
}
|
|
187
189
|
|
|
@@ -535,17 +537,18 @@ function renderHtml(data) {
|
|
|
535
537
|
const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs);
|
|
536
538
|
const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs);
|
|
537
539
|
const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));
|
|
540
|
+
const titleName = escapeHtml(data.projectName);
|
|
538
541
|
return `<!DOCTYPE html>
|
|
539
542
|
<html lang="en">
|
|
540
543
|
<head>
|
|
541
544
|
<meta charset="utf-8">
|
|
542
545
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
543
|
-
<title
|
|
546
|
+
<title>${titleName} · where-you-at report</title>
|
|
544
547
|
<style>${generateCss()}
|
|
545
548
|
</style>
|
|
546
549
|
</head>
|
|
547
550
|
<body>
|
|
548
|
-
<h1
|
|
551
|
+
<h1>${titleName}</h1>
|
|
549
552
|
<div class="meta">Generated ${escapeHtml(data.timestamp)} · Total wall time: ${totalFormatted}</div>
|
|
550
553
|
|
|
551
554
|
<h2>Summary</h2>
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/frame-parser.ts","../src/package-resolver.ts","../src/reporter/aggregate.ts","../src/reporter/format.ts","../src/reporter/html.ts","../src/reporter.ts","../src/sample-store.ts","../src/sampler.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport type { RawCallFrame, ParsedFrame } from './types.js';\n\n/**\n * Classify a V8 CPU profiler call frame and convert its URL to a filesystem path.\n *\n * Every sampled frame from the V8 profiler passes through this function first.\n * It determines the frame kind (user code, internal, eval, wasm) and for user\n * frames converts the URL to a filesystem path and builds a human-readable\n * function identifier.\n */\nexport function parseFrame(frame: RawCallFrame): ParsedFrame {\n const { url, functionName, lineNumber } = frame;\n\n // Empty URL: V8 internal pseudo-frames like (idle), (root), (gc), (program)\n if (url === '') {\n return { kind: 'internal' };\n }\n\n // Node.js built-in modules -- treated as attributable user frames\n if (url.startsWith('node:')) {\n const functionId = `${functionName || '<anonymous>'}:${lineNumber + 1}`;\n return { kind: 'user', filePath: url, functionId };\n }\n\n // WebAssembly frames\n if (url.startsWith('wasm:')) {\n return { kind: 'wasm' };\n }\n\n // Eval frames\n if (url.includes('eval')) {\n return { kind: 'eval' };\n }\n\n // User code: convert URL to filesystem path\n const filePath = url.startsWith('file://')\n ? fileURLToPath(url)\n : url;\n\n // Build human-readable function identifier (convert 0-based to 1-based line)\n const functionId = `${functionName || '<anonymous>'}:${lineNumber + 1}`;\n\n return { kind: 'user', filePath, functionId };\n}\n","import { readFileSync } from 'node:fs';\nimport { dirname, join, relative, sep } from 'node:path';\n\n/**\n * Resolves an absolute file path to a package name and relative path.\n *\n * For node_modules paths: extracts the package name from the last /node_modules/\n * segment (critical for pnpm virtual store compatibility). Handles scoped packages.\n *\n * For first-party files: walks up directory tree looking for package.json,\n * falls back to 'app' if none found.\n */\nexport class PackageResolver {\n private readonly projectRoot: string;\n private readonly packageJsonCache = new Map<string, string | null>();\n\n constructor(projectRoot: string) {\n this.projectRoot = projectRoot;\n }\n\n resolve(absoluteFilePath: string): { packageName: string; relativePath: string } {\n const nodeModulesSeg = `${sep}node_modules${sep}`;\n const lastIdx = absoluteFilePath.lastIndexOf(nodeModulesSeg);\n\n if (lastIdx !== -1) {\n // node_modules path -- extract package name from LAST /node_modules/ segment\n const afterModules = absoluteFilePath.substring(lastIdx + nodeModulesSeg.length);\n const segments = afterModules.split(sep);\n\n let packageName: string;\n let fileStartIdx: number;\n\n if (segments[0]!.startsWith('@')) {\n // Scoped package: @scope/name\n packageName = `${segments[0]}/${segments[1]}`;\n fileStartIdx = 2;\n } else {\n packageName = segments[0]!;\n fileStartIdx = 1;\n }\n\n const relativePath = segments.slice(fileStartIdx).join('/');\n\n return { packageName, relativePath };\n }\n\n // First-party file -- walk up looking for package.json\n const packageName = this.findPackageName(absoluteFilePath);\n const relativePath = relative(this.projectRoot, absoluteFilePath)\n .split(sep)\n .join('/');\n\n return { packageName, relativePath };\n }\n\n /**\n * Walk up from the file's directory looking for package.json.\n * Cache results to avoid repeated filesystem reads.\n */\n private findPackageName(absoluteFilePath: string): string {\n let dir = dirname(absoluteFilePath);\n\n while (true) {\n const cached = this.packageJsonCache.get(dir);\n if (cached !== undefined) {\n if (cached !== null) {\n return cached;\n }\n // null means we checked this dir and no package.json -- continue up\n } else {\n try {\n const raw = readFileSync(join(dir, 'package.json'), 'utf-8');\n const pkg = JSON.parse(raw) as { name?: string };\n const name = pkg.name ?? null;\n this.packageJsonCache.set(dir, name);\n if (name !== null) {\n return name;\n }\n } catch {\n // No package.json here -- cache as null and continue\n this.packageJsonCache.set(dir, null);\n }\n }\n\n const parent = dirname(dir);\n if (parent === dir) {\n // Reached filesystem root without finding a named package.json\n return 'app';\n }\n dir = parent;\n }\n }\n}\n","/**\n * Transform raw SampleStore data into a sorted, thresholded ReportData structure.\n *\n * Pure function: reads nested Maps from SampleStore, applies a 5% threshold\n * at every level (package, file, function) relative to the total profiled time,\n * aggregates below-threshold entries into an \"other\" count, and sorts by\n * timeUs descending at each level.\n */\n\nimport type { SampleStore } from '../sample-store.js';\nimport type {\n ReportData,\n PackageEntry,\n FileEntry,\n FunctionEntry,\n} from '../types.js';\n\nconst THRESHOLD_PCT = 0.05;\n\n/**\n * Aggregate SampleStore data into a ReportData structure.\n *\n * @param store - SampleStore with accumulated microseconds and sample counts\n * @param projectName - Name of the first-party project (for isFirstParty flag)\n * @returns ReportData with packages sorted desc by time, thresholded at 5%\n */\nexport function aggregate(store: SampleStore, projectName: string): ReportData {\n // 1. Calculate total user-attributed time\n let totalTimeUs = 0;\n for (const fileMap of store.packages.values()) {\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n totalTimeUs += us;\n }\n }\n }\n\n if (totalTimeUs === 0) {\n return {\n timestamp: new Date().toLocaleString(),\n totalTimeUs: 0,\n packages: [],\n otherCount: 0,\n };\n }\n\n const threshold = totalTimeUs * THRESHOLD_PCT;\n const packages: PackageEntry[] = [];\n let topLevelOtherCount = 0;\n\n // 2. Process each package\n for (const [packageName, fileMap] of store.packages) {\n // Sum total time for this package\n let packageTimeUs = 0;\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n packageTimeUs += us;\n }\n }\n\n // Apply threshold at package level\n if (packageTimeUs < threshold) {\n topLevelOtherCount++;\n continue;\n }\n\n // Sum total sample count for this package\n let packageSampleCount = 0;\n const countFileMap = store.sampleCountsByPackage.get(packageName);\n if (countFileMap) {\n for (const countFuncMap of countFileMap.values()) {\n for (const count of countFuncMap.values()) {\n packageSampleCount += count;\n }\n }\n }\n\n // 3. Process files within the package\n const files: FileEntry[] = [];\n let fileOtherCount = 0;\n\n for (const [fileName, funcMap] of fileMap) {\n // Sum time for this file\n let fileTimeUs = 0;\n for (const us of funcMap.values()) {\n fileTimeUs += us;\n }\n\n // Apply threshold at file level (relative to total)\n if (fileTimeUs < threshold) {\n fileOtherCount++;\n continue;\n }\n\n // Sum sample count for this file\n let fileSampleCount = 0;\n const countFuncMap = countFileMap?.get(fileName);\n if (countFuncMap) {\n for (const count of countFuncMap.values()) {\n fileSampleCount += count;\n }\n }\n\n // 4. Process functions within the file\n const functions: FunctionEntry[] = [];\n let funcOtherCount = 0;\n\n for (const [funcName, funcTimeUs] of funcMap) {\n // Apply threshold at function level (relative to total)\n if (funcTimeUs < threshold) {\n funcOtherCount++;\n continue;\n }\n\n const funcSampleCount = countFuncMap?.get(funcName) ?? 0;\n\n functions.push({\n name: funcName,\n timeUs: funcTimeUs,\n pct: (funcTimeUs / totalTimeUs) * 100,\n sampleCount: funcSampleCount,\n });\n }\n\n // Sort functions by timeUs descending\n functions.sort((a, b) => b.timeUs - a.timeUs);\n\n files.push({\n name: fileName,\n timeUs: fileTimeUs,\n pct: (fileTimeUs / totalTimeUs) * 100,\n sampleCount: fileSampleCount,\n functions,\n otherCount: funcOtherCount,\n });\n }\n\n // Sort files by timeUs descending\n files.sort((a, b) => b.timeUs - a.timeUs);\n\n packages.push({\n name: packageName,\n timeUs: packageTimeUs,\n pct: (packageTimeUs / totalTimeUs) * 100,\n isFirstParty: packageName === projectName,\n sampleCount: packageSampleCount,\n files,\n otherCount: fileOtherCount,\n });\n }\n\n // Sort packages by timeUs descending\n packages.sort((a, b) => b.timeUs - a.timeUs);\n\n return {\n timestamp: new Date().toLocaleString(),\n totalTimeUs,\n packages,\n otherCount: topLevelOtherCount,\n };\n}\n","/**\n * Format utilities for the HTML reporter.\n * Pure functions with defined input/output contracts.\n */\n\n/**\n * Convert microseconds to adaptive human-readable time string.\n *\n * - >= 1s: shows seconds with 2 decimal places (e.g. \"1.24s\")\n * - < 1s: shows rounded milliseconds (e.g. \"432ms\")\n * - Sub-millisecond values round up to 1ms (never shows \"0ms\" for nonzero input)\n * - Zero returns \"0ms\"\n */\nexport function formatTime(us: number): string {\n if (us === 0) return '0ms';\n\n const ms = us / 1000;\n\n if (ms >= 1000) {\n const seconds = ms / 1000;\n return `${seconds.toFixed(2)}s`;\n }\n\n const rounded = Math.round(ms);\n return `${rounded < 1 ? 1 : rounded}ms`;\n}\n\n/**\n * Convert microseconds to percentage of total with one decimal place.\n * Returns \"0.0%\" when totalUs is zero (avoids division by zero).\n */\nexport function formatPct(us: number, totalUs: number): string {\n if (totalUs === 0) return '0.0%';\n return `${((us / totalUs) * 100).toFixed(1)}%`;\n}\n\n/**\n * Escape HTML-special characters to prevent broken markup.\n * Handles: & < > \" '\n * Ampersand is replaced first to avoid double-escaping.\n */\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","/**\n * HTML renderer for the profiling report.\n *\n * Generates a self-contained HTML file (inline CSS, no external dependencies)\n * with a summary table and expandable Package > File > Function tree.\n */\n\nimport type { ReportData, PackageEntry, FileEntry } from '../types.js';\nimport { formatTime, formatPct, escapeHtml } from './format.js';\n\nfunction generateCss(): string {\n return `\n :root {\n --bg: #fafbfc;\n --text: #1a1a2e;\n --muted: #8b8fa3;\n --border: #e2e4ea;\n --first-party-accent: #3b6cf5;\n --first-party-bg: #eef2ff;\n --dep-bg: #ffffff;\n --bar-track: #e8eaed;\n --bar-fill: #5b8def;\n --bar-fill-fp: #3b6cf5;\n --other-text: #a0a4b8;\n --table-header-bg: #f4f5f7;\n --shadow: 0 1px 3px rgba(0,0,0,0.06);\n --radius: 6px;\n --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;\n --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n }\n\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n font-family: var(--font-sans);\n background: var(--bg);\n color: var(--text);\n line-height: 1.5;\n padding: 2rem;\n max-width: 960px;\n margin: 0 auto;\n }\n\n h1 {\n font-size: 1.5rem;\n font-weight: 600;\n margin-bottom: 0.25rem;\n }\n\n .meta {\n color: var(--muted);\n font-size: 0.85rem;\n margin-bottom: 2rem;\n }\n\n h2 {\n font-size: 1.1rem;\n font-weight: 600;\n margin-bottom: 0.75rem;\n margin-top: 2rem;\n }\n\n /* Summary table */\n table {\n width: 100%;\n border-collapse: collapse;\n background: #fff;\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n margin-bottom: 1rem;\n }\n\n th {\n text-align: left;\n background: var(--table-header-bg);\n padding: 0.6rem 0.75rem;\n font-size: 0.8rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--muted);\n border-bottom: 1px solid var(--border);\n }\n\n td {\n padding: 0.55rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.9rem;\n }\n\n tr:last-child td { border-bottom: none; }\n\n tr.first-party td:first-child {\n border-left: 3px solid var(--first-party-accent);\n padding-left: calc(0.75rem - 3px);\n }\n\n td.pkg-name { font-family: var(--font-mono); font-size: 0.85rem; }\n td.numeric { text-align: right; font-family: var(--font-mono); font-size: 0.85rem; }\n\n .bar-cell {\n width: 30%;\n padding-right: 1rem;\n }\n\n .bar-container {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n\n .bar-track {\n flex: 1;\n height: 8px;\n background: var(--bar-track);\n border-radius: 4px;\n overflow: hidden;\n }\n\n .bar-fill {\n height: 100%;\n border-radius: 4px;\n background: var(--bar-fill);\n min-width: 1px;\n }\n\n tr.first-party .bar-fill {\n background: var(--bar-fill-fp);\n }\n\n .bar-pct {\n font-family: var(--font-mono);\n font-size: 0.8rem;\n min-width: 3.5em;\n text-align: right;\n }\n\n tr.other-row td {\n color: var(--other-text);\n font-style: italic;\n }\n\n /* Tree */\n .tree {\n background: #fff;\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n }\n\n details {\n border-bottom: 1px solid var(--border);\n }\n\n details:last-child { border-bottom: none; }\n\n details details { border-bottom: 1px solid var(--border); }\n details details:last-child { border-bottom: none; }\n\n summary {\n cursor: pointer;\n list-style: none;\n padding: 0.6rem 0.75rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.9rem;\n user-select: none;\n }\n\n summary::-webkit-details-marker { display: none; }\n\n summary::before {\n content: '\\\\25B6';\n font-size: 0.6rem;\n color: var(--muted);\n transition: transform 0.15s ease;\n flex-shrink: 0;\n }\n\n details[open] > summary::before {\n transform: rotate(90deg);\n }\n\n .tree-name {\n font-family: var(--font-mono);\n font-size: 0.85rem;\n flex: 1;\n }\n\n .tree-label {\n font-family: var(--font-sans);\n font-size: 0.65rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n padding: 0.1rem 0.35rem;\n border-radius: 3px;\n flex-shrink: 0;\n }\n\n .tree-label.pkg { background: #e8eaed; color: #555; }\n .tree-label.file { background: #e8f0fe; color: #3b6cf5; }\n .tree-label.fn { background: #f0f0f0; color: #777; }\n\n .tree-stats {\n font-family: var(--font-mono);\n font-size: 0.8rem;\n color: var(--muted);\n flex-shrink: 0;\n }\n\n /* Level indentation */\n .level-0 > summary { padding-left: 0.75rem; }\n .level-1 > summary { padding-left: 2rem; }\n .level-2 { padding: 0.45rem 0.75rem 0.45rem 3.25rem; font-size: 0.85rem; display: flex; align-items: center; gap: 0.5rem; }\n\n /* First-party package highlight */\n .fp-pkg > summary {\n background: var(--first-party-bg);\n border-left: 3px solid var(--first-party-accent);\n }\n\n .other-item {\n padding: 0.45rem 0.75rem;\n color: var(--other-text);\n font-style: italic;\n font-size: 0.85rem;\n }\n\n .other-item.indent-1 { padding-left: 2rem; }\n .other-item.indent-2 { padding-left: 3.25rem; }\n\n @media (max-width: 600px) {\n body { padding: 1rem; }\n .bar-cell { width: 25%; }\n }\n `;\n}\n\nfunction renderSummaryTable(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n): string {\n let rows = '';\n\n for (const pkg of packages) {\n const cls = pkg.isFirstParty ? 'first-party' : 'dependency';\n const pctVal = totalTimeUs > 0 ? (pkg.timeUs / totalTimeUs) * 100 : 0;\n rows += `\n <tr class=\"${cls}\">\n <td class=\"pkg-name\">${escapeHtml(pkg.name)}</td>\n <td class=\"numeric\">${escapeHtml(formatTime(pkg.timeUs))}</td>\n <td class=\"bar-cell\">\n <div class=\"bar-container\">\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${pctVal.toFixed(1)}%\"></div></div>\n <span class=\"bar-pct\">${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))}</span>\n </div>\n </td>\n <td class=\"numeric\">${pkg.sampleCount}</td>\n </tr>`;\n }\n\n if (otherCount > 0) {\n rows += `\n <tr class=\"other-row\">\n <td class=\"pkg-name\">Other (${otherCount} items)</td>\n <td class=\"numeric\"></td>\n <td class=\"bar-cell\"></td>\n <td class=\"numeric\"></td>\n </tr>`;\n }\n\n return `\n <table>\n <thead>\n <tr>\n <th>Package</th>\n <th>Wall Time</th>\n <th>% of Total</th>\n <th>Samples</th>\n </tr>\n </thead>\n <tbody>${rows}\n </tbody>\n </table>`;\n}\n\nfunction renderTree(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n): string {\n let html = '<div class=\"tree\">';\n\n for (const pkg of packages) {\n const fpCls = pkg.isFirstParty ? ' fp-pkg' : '';\n html += `<details class=\"level-0${fpCls}\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label pkg\">pkg</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(pkg.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;\n html += `</summary>`;\n\n for (const file of pkg.files) {\n html += `<details class=\"level-1\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label file\">file</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(file.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(file.timeUs))} · ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} · ${file.sampleCount} samples</span>`;\n html += `</summary>`;\n\n for (const fn of file.functions) {\n html += `<div class=\"level-2\">`;\n html += `<span class=\"tree-label fn\">fn</span> `;\n html += `<span class=\"tree-name\">${escapeHtml(fn.name)}</span>`;\n html += ` <span class=\"tree-stats\">${escapeHtml(formatTime(fn.timeUs))} · ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} · ${fn.sampleCount} samples</span>`;\n html += `</div>`;\n }\n\n if (file.otherCount > 0) {\n html += `<div class=\"other-item indent-2\">Other (${file.otherCount} items)</div>`;\n }\n\n html += `</details>`;\n }\n\n if (pkg.otherCount > 0) {\n html += `<div class=\"other-item indent-1\">Other (${pkg.otherCount} items)</div>`;\n }\n\n html += `</details>`;\n }\n\n if (otherCount > 0) {\n html += `<div class=\"other-item\">Other (${otherCount} packages)</div>`;\n }\n\n html += '</div>';\n return html;\n}\n\n/**\n * Render a complete self-contained HTML report from aggregated profiling data.\n */\nexport function renderHtml(data: ReportData): string {\n const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs);\n const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs);\n const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>where-you-at report</title>\n <style>${generateCss()}\n </style>\n</head>\n<body>\n <h1>where-you-at</h1>\n <div class=\"meta\">Generated ${escapeHtml(data.timestamp)} · Total wall time: ${totalFormatted}</div>\n\n <h2>Summary</h2>\n ${summaryTable}\n\n <h2>Details</h2>\n ${tree}\n</body>\n</html>`;\n}\n","/**\n * Reporter orchestrator.\n *\n * Aggregates SampleStore data, renders HTML, writes file to cwd,\n * and returns the file path.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { SampleStore } from './sample-store.js';\nimport { aggregate } from './reporter/aggregate.js';\nimport { renderHtml } from './reporter/html.js';\n\nfunction generateFilename(): string {\n const now = new Date();\n const pad = (n: number) => String(n).padStart(2, '0');\n const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;\n const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;\n return `where-you-at-${date}-${time}.html`;\n}\n\nfunction readProjectName(cwd: string): string {\n try {\n const raw = readFileSync(join(cwd, 'package.json'), 'utf-8');\n const pkg = JSON.parse(raw) as { name?: string };\n return pkg.name ?? 'app';\n } catch {\n return 'app';\n }\n}\n\nexport function generateReport(store: SampleStore, cwd?: string): string {\n const resolvedCwd = cwd ?? process.cwd();\n const projectName = readProjectName(resolvedCwd);\n const data = aggregate(store, projectName);\n const html = renderHtml(data);\n const filename = generateFilename();\n const filepath = join(resolvedCwd, filename);\n writeFileSync(filepath, html, 'utf-8');\n console.log(`Report written to ./${filename}`);\n return filepath;\n}\n","/**\n * Accumulates per-package wall time (microseconds) from the V8 CPU profiler.\n *\n * Data structure: nested Maps -- package -> file -> function -> microseconds.\n * This naturally matches the package-first tree output that the reporter\n * needs in Phase 3. O(1) lookups at each level, no serialization overhead.\n *\n * A parallel sampleCounts structure tracks raw sample counts (incremented by 1\n * per record() call) for the summary table's \"Sample count\" column.\n */\nexport class SampleStore {\n private data = new Map<string, Map<string, Map<string, number>>>();\n private counts = new Map<string, Map<string, Map<string, number>>>();\n private internalCount = 0;\n private internalSamples = 0;\n\n /**\n * Record a sample for a user-code frame.\n * Accumulates deltaUs microseconds for the given (package, file, function) triple,\n * and increments the parallel sample count by 1.\n */\n record(packageName: string, relativePath: string, functionId: string, deltaUs: number): void {\n // Accumulate microseconds\n let fileMap = this.data.get(packageName);\n if (fileMap === undefined) {\n fileMap = new Map<string, Map<string, number>>();\n this.data.set(packageName, fileMap);\n }\n\n let funcMap = fileMap.get(relativePath);\n if (funcMap === undefined) {\n funcMap = new Map<string, number>();\n fileMap.set(relativePath, funcMap);\n }\n\n funcMap.set(functionId, (funcMap.get(functionId) ?? 0) + deltaUs);\n\n // Parallel sample count (always +1)\n let countFileMap = this.counts.get(packageName);\n if (countFileMap === undefined) {\n countFileMap = new Map<string, Map<string, number>>();\n this.counts.set(packageName, countFileMap);\n }\n\n let countFuncMap = countFileMap.get(relativePath);\n if (countFuncMap === undefined) {\n countFuncMap = new Map<string, number>();\n countFileMap.set(relativePath, countFuncMap);\n }\n\n countFuncMap.set(functionId, (countFuncMap.get(functionId) ?? 0) + 1);\n }\n\n /** Record an internal/filtered frame (empty URL, eval, wasm, idle, etc). */\n recordInternal(deltaUs: number): void {\n this.internalCount += deltaUs;\n this.internalSamples += 1;\n }\n\n /** Reset all accumulated data to a clean state. */\n clear(): void {\n this.data = new Map<string, Map<string, Map<string, number>>>();\n this.counts = new Map<string, Map<string, Map<string, number>>>();\n this.internalCount = 0;\n this.internalSamples = 0;\n }\n\n /** Read-only access to the accumulated sample data (microseconds). */\n get packages(): ReadonlyMap<string, Map<string, Map<string, number>>> {\n return this.data;\n }\n\n /** Count of internal/filtered microseconds recorded. */\n get internal(): number {\n return this.internalCount;\n }\n\n /** Read-only access to the parallel sample counts. */\n get sampleCountsByPackage(): ReadonlyMap<string, Map<string, Map<string, number>>> {\n return this.counts;\n }\n\n /** Count of internal/filtered samples (raw count, not microseconds). */\n get internalSampleCount(): number {\n return this.internalSamples;\n }\n}\n","import { Session } from 'node:inspector/promises';\nimport type { Profiler } from 'node:inspector';\nimport { parseFrame } from './frame-parser.js';\nimport { PackageResolver } from './package-resolver.js';\nimport { generateReport } from './reporter.js';\nimport { SampleStore } from './sample-store.js';\nimport type { RawCallFrame } from './types.js';\n\n// Module-level state -- lazy initialization\nlet session: Session | null = null;\nlet profiling = false;\nconst store = new SampleStore();\nconst resolver = new PackageResolver(process.cwd());\n\n/**\n * Start the V8 CPU profiler. If already profiling, this is a safe no-op.\n */\nexport async function track(options?: { interval?: number }): Promise<void> {\n if (profiling) return;\n\n if (session === null) {\n session = new Session();\n session.connect();\n }\n\n await session.post('Profiler.enable');\n\n if (options?.interval !== undefined) {\n await session.post('Profiler.setSamplingInterval', {\n interval: options.interval,\n });\n }\n\n await session.post('Profiler.start');\n profiling = true;\n}\n\n/**\n * Stop the profiler (if running) and reset all accumulated sample data.\n */\nexport async function clear(): Promise<void> {\n if (profiling && session) {\n await session.post('Profiler.stop');\n await session.post('Profiler.disable');\n profiling = false;\n }\n store.clear();\n}\n\n/**\n * Stop the profiler, process collected samples through the data pipeline\n * (parseFrame -> PackageResolver -> SampleStore), generate an HTML report,\n * and return the file path. Resets the store after reporting (clean slate\n * for next cycle).\n *\n * Returns the absolute path to the generated HTML file, or empty string\n * if no samples were collected.\n */\nexport async function report(): Promise<string> {\n if (!profiling || !session) {\n console.log('no samples collected');\n return '';\n }\n\n const { profile } = await session.post('Profiler.stop');\n await session.post('Profiler.disable');\n profiling = false;\n\n processProfile(profile);\n\n let filepath = '';\n\n if (store.packages.size > 0) {\n filepath = generateReport(store);\n } else {\n console.log('no samples collected');\n }\n\n store.clear();\n return filepath;\n}\n\n/**\n * Process a V8 CPUProfile: walk each sample, parse the frame, resolve\n * the package, and record into the store. Uses timeDeltas for wall-time\n * microsecond accumulation.\n */\nfunction processProfile(profile: Profiler.Profile): void {\n const nodeMap = new Map(profile.nodes.map((n) => [n.id, n]));\n const samples = profile.samples ?? [];\n const timeDeltas = profile.timeDeltas ?? [];\n\n for (let i = 0; i < samples.length; i++) {\n const node = nodeMap.get(samples[i]!);\n if (!node) continue;\n\n const deltaUs = timeDeltas[i] ?? 0;\n const parsed = parseFrame(node.callFrame as RawCallFrame);\n\n if (parsed.kind === 'user') {\n if (parsed.filePath.startsWith('node:')) {\n // Node.js built-in: attribute to \"node (built-in)\" package\n const relativePath = parsed.filePath.slice(5);\n store.record('node (built-in)', relativePath, parsed.functionId, deltaUs);\n } else {\n const { packageName, relativePath } = resolver.resolve(parsed.filePath);\n store.record(packageName, relativePath, parsed.functionId, deltaUs);\n }\n } else {\n store.recordInternal(deltaUs);\n }\n }\n}\n\n/** @internal -- exposed for testing only */\nexport function _getStore(): SampleStore {\n return store;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/frame-parser.ts","../src/package-resolver.ts","../src/reporter/aggregate.ts","../src/reporter/format.ts","../src/reporter/html.ts","../src/reporter.ts","../src/sample-store.ts","../src/sampler.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport type { RawCallFrame, ParsedFrame } from './types.js';\n\n/**\n * Classify a V8 CPU profiler call frame and convert its URL to a filesystem path.\n *\n * Every sampled frame from the V8 profiler passes through this function first.\n * It determines the frame kind (user code, internal, eval, wasm) and for user\n * frames converts the URL to a filesystem path and builds a human-readable\n * function identifier.\n */\nexport function parseFrame(frame: RawCallFrame): ParsedFrame {\n const { url, functionName, lineNumber } = frame;\n\n // Empty URL: V8 internal pseudo-frames like (idle), (root), (gc), (program)\n if (url === '') {\n return { kind: 'internal' };\n }\n\n // Node.js built-in modules -- treated as attributable user frames\n if (url.startsWith('node:')) {\n const functionId = `${functionName || '<anonymous>'}:${lineNumber + 1}`;\n return { kind: 'user', filePath: url, functionId };\n }\n\n // WebAssembly frames\n if (url.startsWith('wasm:')) {\n return { kind: 'wasm' };\n }\n\n // Eval frames\n if (url.includes('eval')) {\n return { kind: 'eval' };\n }\n\n // User code: convert URL to filesystem path\n const filePath = url.startsWith('file://')\n ? fileURLToPath(url)\n : url;\n\n // Build human-readable function identifier (convert 0-based to 1-based line)\n const functionId = `${functionName || '<anonymous>'}:${lineNumber + 1}`;\n\n return { kind: 'user', filePath, functionId };\n}\n","import { readFileSync } from 'node:fs';\nimport { dirname, join, relative, sep } from 'node:path';\n\n/**\n * Resolves an absolute file path to a package name and relative path.\n *\n * For node_modules paths: extracts the package name from the last /node_modules/\n * segment (critical for pnpm virtual store compatibility). Handles scoped packages.\n *\n * For first-party files: walks up directory tree looking for package.json,\n * falls back to 'app' if none found.\n */\nexport class PackageResolver {\n private readonly projectRoot: string;\n private readonly packageJsonCache = new Map<string, string | null>();\n\n constructor(projectRoot: string) {\n this.projectRoot = projectRoot;\n }\n\n resolve(absoluteFilePath: string): { packageName: string; relativePath: string } {\n const nodeModulesSeg = `${sep}node_modules${sep}`;\n const lastIdx = absoluteFilePath.lastIndexOf(nodeModulesSeg);\n\n if (lastIdx !== -1) {\n // node_modules path -- extract package name from LAST /node_modules/ segment\n const afterModules = absoluteFilePath.substring(lastIdx + nodeModulesSeg.length);\n const segments = afterModules.split(sep);\n\n let packageName: string;\n let fileStartIdx: number;\n\n if (segments[0]!.startsWith('@')) {\n // Scoped package: @scope/name\n packageName = `${segments[0]}/${segments[1]}`;\n fileStartIdx = 2;\n } else {\n packageName = segments[0]!;\n fileStartIdx = 1;\n }\n\n const relativePath = segments.slice(fileStartIdx).join('/');\n\n return { packageName, relativePath };\n }\n\n // First-party file -- walk up looking for package.json\n const packageName = this.findPackageName(absoluteFilePath);\n const relativePath = relative(this.projectRoot, absoluteFilePath)\n .split(sep)\n .join('/');\n\n return { packageName, relativePath };\n }\n\n /**\n * Walk up from the file's directory looking for package.json.\n * Cache results to avoid repeated filesystem reads.\n */\n private findPackageName(absoluteFilePath: string): string {\n let dir = dirname(absoluteFilePath);\n\n while (true) {\n const cached = this.packageJsonCache.get(dir);\n if (cached !== undefined) {\n if (cached !== null) {\n return cached;\n }\n // null means we checked this dir and no package.json -- continue up\n } else {\n try {\n const raw = readFileSync(join(dir, 'package.json'), 'utf-8');\n const pkg = JSON.parse(raw) as { name?: string };\n const name = pkg.name ?? null;\n this.packageJsonCache.set(dir, name);\n if (name !== null) {\n return name;\n }\n } catch {\n // No package.json here -- cache as null and continue\n this.packageJsonCache.set(dir, null);\n }\n }\n\n const parent = dirname(dir);\n if (parent === dir) {\n // Reached filesystem root without finding a named package.json\n return 'app';\n }\n dir = parent;\n }\n }\n}\n","/**\n * Transform raw SampleStore data into a sorted, thresholded ReportData structure.\n *\n * Pure function: reads nested Maps from SampleStore, applies a 5% threshold\n * at every level (package, file, function) relative to the total profiled time,\n * aggregates below-threshold entries into an \"other\" count, and sorts by\n * timeUs descending at each level.\n */\n\nimport type { SampleStore } from '../sample-store.js';\nimport type {\n ReportData,\n PackageEntry,\n FileEntry,\n FunctionEntry,\n} from '../types.js';\n\nconst THRESHOLD_PCT = 0.05;\n\n/**\n * Aggregate SampleStore data into a ReportData structure.\n *\n * @param store - SampleStore with accumulated microseconds and sample counts\n * @param projectName - Name of the first-party project (for isFirstParty flag)\n * @returns ReportData with packages sorted desc by time, thresholded at 5%\n */\nexport function aggregate(store: SampleStore, projectName: string): ReportData {\n // 1. Calculate total user-attributed time\n let totalTimeUs = 0;\n for (const fileMap of store.packages.values()) {\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n totalTimeUs += us;\n }\n }\n }\n\n if (totalTimeUs === 0) {\n return {\n timestamp: new Date().toLocaleString(),\n totalTimeUs: 0,\n packages: [],\n otherCount: 0,\n projectName,\n };\n }\n\n const threshold = totalTimeUs * THRESHOLD_PCT;\n const packages: PackageEntry[] = [];\n let topLevelOtherCount = 0;\n\n // 2. Process each package\n for (const [packageName, fileMap] of store.packages) {\n // Sum total time for this package\n let packageTimeUs = 0;\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n packageTimeUs += us;\n }\n }\n\n // Apply threshold at package level\n if (packageTimeUs < threshold) {\n topLevelOtherCount++;\n continue;\n }\n\n // Sum total sample count for this package\n let packageSampleCount = 0;\n const countFileMap = store.sampleCountsByPackage.get(packageName);\n if (countFileMap) {\n for (const countFuncMap of countFileMap.values()) {\n for (const count of countFuncMap.values()) {\n packageSampleCount += count;\n }\n }\n }\n\n // 3. Process files within the package\n const files: FileEntry[] = [];\n let fileOtherCount = 0;\n\n for (const [fileName, funcMap] of fileMap) {\n // Sum time for this file\n let fileTimeUs = 0;\n for (const us of funcMap.values()) {\n fileTimeUs += us;\n }\n\n // Apply threshold at file level (relative to total)\n if (fileTimeUs < threshold) {\n fileOtherCount++;\n continue;\n }\n\n // Sum sample count for this file\n let fileSampleCount = 0;\n const countFuncMap = countFileMap?.get(fileName);\n if (countFuncMap) {\n for (const count of countFuncMap.values()) {\n fileSampleCount += count;\n }\n }\n\n // 4. Process functions within the file\n const functions: FunctionEntry[] = [];\n let funcOtherCount = 0;\n\n for (const [funcName, funcTimeUs] of funcMap) {\n // Apply threshold at function level (relative to total)\n if (funcTimeUs < threshold) {\n funcOtherCount++;\n continue;\n }\n\n const funcSampleCount = countFuncMap?.get(funcName) ?? 0;\n\n functions.push({\n name: funcName,\n timeUs: funcTimeUs,\n pct: (funcTimeUs / totalTimeUs) * 100,\n sampleCount: funcSampleCount,\n });\n }\n\n // Sort functions by timeUs descending\n functions.sort((a, b) => b.timeUs - a.timeUs);\n\n files.push({\n name: fileName,\n timeUs: fileTimeUs,\n pct: (fileTimeUs / totalTimeUs) * 100,\n sampleCount: fileSampleCount,\n functions,\n otherCount: funcOtherCount,\n });\n }\n\n // Sort files by timeUs descending\n files.sort((a, b) => b.timeUs - a.timeUs);\n\n packages.push({\n name: packageName,\n timeUs: packageTimeUs,\n pct: (packageTimeUs / totalTimeUs) * 100,\n isFirstParty: packageName === projectName,\n sampleCount: packageSampleCount,\n files,\n otherCount: fileOtherCount,\n });\n }\n\n // Sort packages by timeUs descending\n packages.sort((a, b) => b.timeUs - a.timeUs);\n\n return {\n timestamp: new Date().toLocaleString(),\n totalTimeUs,\n packages,\n otherCount: topLevelOtherCount,\n projectName,\n };\n}\n","/**\n * Format utilities for the HTML reporter.\n * Pure functions with defined input/output contracts.\n */\n\n/**\n * Convert microseconds to adaptive human-readable time string.\n *\n * - >= 1s: shows seconds with 2 decimal places (e.g. \"1.24s\")\n * - < 1s: shows rounded milliseconds (e.g. \"432ms\")\n * - Sub-millisecond values round up to 1ms (never shows \"0ms\" for nonzero input)\n * - Zero returns \"0ms\"\n */\nexport function formatTime(us: number): string {\n if (us === 0) return '0ms';\n\n const ms = us / 1000;\n\n if (ms >= 1000) {\n const seconds = ms / 1000;\n return `${seconds.toFixed(2)}s`;\n }\n\n const rounded = Math.round(ms);\n return `${rounded < 1 ? 1 : rounded}ms`;\n}\n\n/**\n * Convert microseconds to percentage of total with one decimal place.\n * Returns \"0.0%\" when totalUs is zero (avoids division by zero).\n */\nexport function formatPct(us: number, totalUs: number): string {\n if (totalUs === 0) return '0.0%';\n return `${((us / totalUs) * 100).toFixed(1)}%`;\n}\n\n/**\n * Escape HTML-special characters to prevent broken markup.\n * Handles: & < > \" '\n * Ampersand is replaced first to avoid double-escaping.\n */\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","/**\n * HTML renderer for the profiling report.\n *\n * Generates a self-contained HTML file (inline CSS, no external dependencies)\n * with a summary table and expandable Package > File > Function tree.\n */\n\nimport type { ReportData, PackageEntry, FileEntry } from '../types.js';\nimport { formatTime, formatPct, escapeHtml } from './format.js';\n\nfunction generateCss(): string {\n return `\n :root {\n --bg: #fafbfc;\n --text: #1a1a2e;\n --muted: #8b8fa3;\n --border: #e2e4ea;\n --first-party-accent: #3b6cf5;\n --first-party-bg: #eef2ff;\n --dep-bg: #ffffff;\n --bar-track: #e8eaed;\n --bar-fill: #5b8def;\n --bar-fill-fp: #3b6cf5;\n --other-text: #a0a4b8;\n --table-header-bg: #f4f5f7;\n --shadow: 0 1px 3px rgba(0,0,0,0.06);\n --radius: 6px;\n --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;\n --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n }\n\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n font-family: var(--font-sans);\n background: var(--bg);\n color: var(--text);\n line-height: 1.5;\n padding: 2rem;\n max-width: 960px;\n margin: 0 auto;\n }\n\n h1 {\n font-size: 1.5rem;\n font-weight: 600;\n margin-bottom: 0.25rem;\n }\n\n .meta {\n color: var(--muted);\n font-size: 0.85rem;\n margin-bottom: 2rem;\n }\n\n h2 {\n font-size: 1.1rem;\n font-weight: 600;\n margin-bottom: 0.75rem;\n margin-top: 2rem;\n }\n\n /* Summary table */\n table {\n width: 100%;\n border-collapse: collapse;\n background: #fff;\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n margin-bottom: 1rem;\n }\n\n th {\n text-align: left;\n background: var(--table-header-bg);\n padding: 0.6rem 0.75rem;\n font-size: 0.8rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--muted);\n border-bottom: 1px solid var(--border);\n }\n\n td {\n padding: 0.55rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.9rem;\n }\n\n tr:last-child td { border-bottom: none; }\n\n tr.first-party td:first-child {\n border-left: 3px solid var(--first-party-accent);\n padding-left: calc(0.75rem - 3px);\n }\n\n td.pkg-name { font-family: var(--font-mono); font-size: 0.85rem; }\n td.numeric { text-align: right; font-family: var(--font-mono); font-size: 0.85rem; }\n\n .bar-cell {\n width: 30%;\n padding-right: 1rem;\n }\n\n .bar-container {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n\n .bar-track {\n flex: 1;\n height: 8px;\n background: var(--bar-track);\n border-radius: 4px;\n overflow: hidden;\n }\n\n .bar-fill {\n height: 100%;\n border-radius: 4px;\n background: var(--bar-fill);\n min-width: 1px;\n }\n\n tr.first-party .bar-fill {\n background: var(--bar-fill-fp);\n }\n\n .bar-pct {\n font-family: var(--font-mono);\n font-size: 0.8rem;\n min-width: 3.5em;\n text-align: right;\n }\n\n tr.other-row td {\n color: var(--other-text);\n font-style: italic;\n }\n\n /* Tree */\n .tree {\n background: #fff;\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n overflow: hidden;\n }\n\n details {\n border-bottom: 1px solid var(--border);\n }\n\n details:last-child { border-bottom: none; }\n\n details details { border-bottom: 1px solid var(--border); }\n details details:last-child { border-bottom: none; }\n\n summary {\n cursor: pointer;\n list-style: none;\n padding: 0.6rem 0.75rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.9rem;\n user-select: none;\n }\n\n summary::-webkit-details-marker { display: none; }\n\n summary::before {\n content: '\\\\25B6';\n font-size: 0.6rem;\n color: var(--muted);\n transition: transform 0.15s ease;\n flex-shrink: 0;\n }\n\n details[open] > summary::before {\n transform: rotate(90deg);\n }\n\n .tree-name {\n font-family: var(--font-mono);\n font-size: 0.85rem;\n flex: 1;\n }\n\n .tree-label {\n font-family: var(--font-sans);\n font-size: 0.65rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n padding: 0.1rem 0.35rem;\n border-radius: 3px;\n flex-shrink: 0;\n }\n\n .tree-label.pkg { background: #e8eaed; color: #555; }\n .tree-label.file { background: #e8f0fe; color: #3b6cf5; }\n .tree-label.fn { background: #f0f0f0; color: #777; }\n\n .tree-stats {\n font-family: var(--font-mono);\n font-size: 0.8rem;\n color: var(--muted);\n flex-shrink: 0;\n }\n\n /* Level indentation */\n .level-0 > summary { padding-left: 0.75rem; }\n .level-1 > summary { padding-left: 2rem; }\n .level-2 { padding: 0.45rem 0.75rem 0.45rem 3.25rem; font-size: 0.85rem; display: flex; align-items: center; gap: 0.5rem; }\n\n /* First-party package highlight */\n .fp-pkg > summary {\n background: var(--first-party-bg);\n border-left: 3px solid var(--first-party-accent);\n }\n\n .other-item {\n padding: 0.45rem 0.75rem;\n color: var(--other-text);\n font-style: italic;\n font-size: 0.85rem;\n }\n\n .other-item.indent-1 { padding-left: 2rem; }\n .other-item.indent-2 { padding-left: 3.25rem; }\n\n @media (max-width: 600px) {\n body { padding: 1rem; }\n .bar-cell { width: 25%; }\n }\n `;\n}\n\nfunction renderSummaryTable(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n): string {\n let rows = '';\n\n for (const pkg of packages) {\n const cls = pkg.isFirstParty ? 'first-party' : 'dependency';\n const pctVal = totalTimeUs > 0 ? (pkg.timeUs / totalTimeUs) * 100 : 0;\n rows += `\n <tr class=\"${cls}\">\n <td class=\"pkg-name\">${escapeHtml(pkg.name)}</td>\n <td class=\"numeric\">${escapeHtml(formatTime(pkg.timeUs))}</td>\n <td class=\"bar-cell\">\n <div class=\"bar-container\">\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${pctVal.toFixed(1)}%\"></div></div>\n <span class=\"bar-pct\">${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))}</span>\n </div>\n </td>\n <td class=\"numeric\">${pkg.sampleCount}</td>\n </tr>`;\n }\n\n if (otherCount > 0) {\n rows += `\n <tr class=\"other-row\">\n <td class=\"pkg-name\">Other (${otherCount} items)</td>\n <td class=\"numeric\"></td>\n <td class=\"bar-cell\"></td>\n <td class=\"numeric\"></td>\n </tr>`;\n }\n\n return `\n <table>\n <thead>\n <tr>\n <th>Package</th>\n <th>Wall Time</th>\n <th>% of Total</th>\n <th>Samples</th>\n </tr>\n </thead>\n <tbody>${rows}\n </tbody>\n </table>`;\n}\n\nfunction renderTree(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n): string {\n let html = '<div class=\"tree\">';\n\n for (const pkg of packages) {\n const fpCls = pkg.isFirstParty ? ' fp-pkg' : '';\n html += `<details class=\"level-0${fpCls}\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label pkg\">pkg</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(pkg.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;\n html += `</summary>`;\n\n for (const file of pkg.files) {\n html += `<details class=\"level-1\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label file\">file</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(file.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(file.timeUs))} · ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} · ${file.sampleCount} samples</span>`;\n html += `</summary>`;\n\n for (const fn of file.functions) {\n html += `<div class=\"level-2\">`;\n html += `<span class=\"tree-label fn\">fn</span> `;\n html += `<span class=\"tree-name\">${escapeHtml(fn.name)}</span>`;\n html += ` <span class=\"tree-stats\">${escapeHtml(formatTime(fn.timeUs))} · ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} · ${fn.sampleCount} samples</span>`;\n html += `</div>`;\n }\n\n if (file.otherCount > 0) {\n html += `<div class=\"other-item indent-2\">Other (${file.otherCount} items)</div>`;\n }\n\n html += `</details>`;\n }\n\n if (pkg.otherCount > 0) {\n html += `<div class=\"other-item indent-1\">Other (${pkg.otherCount} items)</div>`;\n }\n\n html += `</details>`;\n }\n\n if (otherCount > 0) {\n html += `<div class=\"other-item\">Other (${otherCount} packages)</div>`;\n }\n\n html += '</div>';\n return html;\n}\n\n/**\n * Render a complete self-contained HTML report from aggregated profiling data.\n */\nexport function renderHtml(data: ReportData): string {\n const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs);\n const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs);\n const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));\n\n const titleName = escapeHtml(data.projectName);\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>${titleName} · where-you-at report</title>\n <style>${generateCss()}\n </style>\n</head>\n<body>\n <h1>${titleName}</h1>\n <div class=\"meta\">Generated ${escapeHtml(data.timestamp)} · Total wall time: ${totalFormatted}</div>\n\n <h2>Summary</h2>\n ${summaryTable}\n\n <h2>Details</h2>\n ${tree}\n</body>\n</html>`;\n}\n","/**\n * Reporter orchestrator.\n *\n * Aggregates SampleStore data, renders HTML, writes file to cwd,\n * and returns the file path.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { SampleStore } from './sample-store.js';\nimport { aggregate } from './reporter/aggregate.js';\nimport { renderHtml } from './reporter/html.js';\n\nfunction generateFilename(): string {\n const now = new Date();\n const pad = (n: number) => String(n).padStart(2, '0');\n const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;\n const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;\n return `where-you-at-${date}-${time}.html`;\n}\n\nfunction readProjectName(cwd: string): string {\n try {\n const raw = readFileSync(join(cwd, 'package.json'), 'utf-8');\n const pkg = JSON.parse(raw) as { name?: string };\n return pkg.name ?? 'app';\n } catch {\n return 'app';\n }\n}\n\nexport function generateReport(store: SampleStore, cwd?: string): string {\n const resolvedCwd = cwd ?? process.cwd();\n const projectName = readProjectName(resolvedCwd);\n const data = aggregate(store, projectName);\n const html = renderHtml(data);\n const filename = generateFilename();\n const filepath = join(resolvedCwd, filename);\n writeFileSync(filepath, html, 'utf-8');\n console.log(`Report written to ./${filename}`);\n return filepath;\n}\n","/**\n * Accumulates per-package wall time (microseconds) from the V8 CPU profiler.\n *\n * Data structure: nested Maps -- package -> file -> function -> microseconds.\n * This naturally matches the package-first tree output that the reporter\n * needs in Phase 3. O(1) lookups at each level, no serialization overhead.\n *\n * A parallel sampleCounts structure tracks raw sample counts (incremented by 1\n * per record() call) for the summary table's \"Sample count\" column.\n */\nexport class SampleStore {\n private data = new Map<string, Map<string, Map<string, number>>>();\n private counts = new Map<string, Map<string, Map<string, number>>>();\n private internalCount = 0;\n private internalSamples = 0;\n\n /**\n * Record a sample for a user-code frame.\n * Accumulates deltaUs microseconds for the given (package, file, function) triple,\n * and increments the parallel sample count by 1.\n */\n record(packageName: string, relativePath: string, functionId: string, deltaUs: number): void {\n // Accumulate microseconds\n let fileMap = this.data.get(packageName);\n if (fileMap === undefined) {\n fileMap = new Map<string, Map<string, number>>();\n this.data.set(packageName, fileMap);\n }\n\n let funcMap = fileMap.get(relativePath);\n if (funcMap === undefined) {\n funcMap = new Map<string, number>();\n fileMap.set(relativePath, funcMap);\n }\n\n funcMap.set(functionId, (funcMap.get(functionId) ?? 0) + deltaUs);\n\n // Parallel sample count (always +1)\n let countFileMap = this.counts.get(packageName);\n if (countFileMap === undefined) {\n countFileMap = new Map<string, Map<string, number>>();\n this.counts.set(packageName, countFileMap);\n }\n\n let countFuncMap = countFileMap.get(relativePath);\n if (countFuncMap === undefined) {\n countFuncMap = new Map<string, number>();\n countFileMap.set(relativePath, countFuncMap);\n }\n\n countFuncMap.set(functionId, (countFuncMap.get(functionId) ?? 0) + 1);\n }\n\n /** Record an internal/filtered frame (empty URL, eval, wasm, idle, etc). */\n recordInternal(deltaUs: number): void {\n this.internalCount += deltaUs;\n this.internalSamples += 1;\n }\n\n /** Reset all accumulated data to a clean state. */\n clear(): void {\n this.data = new Map<string, Map<string, Map<string, number>>>();\n this.counts = new Map<string, Map<string, Map<string, number>>>();\n this.internalCount = 0;\n this.internalSamples = 0;\n }\n\n /** Read-only access to the accumulated sample data (microseconds). */\n get packages(): ReadonlyMap<string, Map<string, Map<string, number>>> {\n return this.data;\n }\n\n /** Count of internal/filtered microseconds recorded. */\n get internal(): number {\n return this.internalCount;\n }\n\n /** Read-only access to the parallel sample counts. */\n get sampleCountsByPackage(): ReadonlyMap<string, Map<string, Map<string, number>>> {\n return this.counts;\n }\n\n /** Count of internal/filtered samples (raw count, not microseconds). */\n get internalSampleCount(): number {\n return this.internalSamples;\n }\n}\n","import { Session } from 'node:inspector/promises';\nimport type { Profiler } from 'node:inspector';\nimport { parseFrame } from './frame-parser.js';\nimport { PackageResolver } from './package-resolver.js';\nimport { generateReport } from './reporter.js';\nimport { SampleStore } from './sample-store.js';\nimport type { RawCallFrame } from './types.js';\n\n// Module-level state -- lazy initialization\nlet session: Session | null = null;\nlet profiling = false;\nconst store = new SampleStore();\nconst resolver = new PackageResolver(process.cwd());\n\n/**\n * Start the V8 CPU profiler. If already profiling, this is a safe no-op.\n */\nexport async function track(options?: { interval?: number }): Promise<void> {\n if (profiling) return;\n\n if (session === null) {\n session = new Session();\n session.connect();\n }\n\n await session.post('Profiler.enable');\n\n if (options?.interval !== undefined) {\n await session.post('Profiler.setSamplingInterval', {\n interval: options.interval,\n });\n }\n\n await session.post('Profiler.start');\n profiling = true;\n}\n\n/**\n * Stop the profiler (if running) and reset all accumulated sample data.\n */\nexport async function clear(): Promise<void> {\n if (profiling && session) {\n await session.post('Profiler.stop');\n await session.post('Profiler.disable');\n profiling = false;\n }\n store.clear();\n}\n\n/**\n * Stop the profiler, process collected samples through the data pipeline\n * (parseFrame -> PackageResolver -> SampleStore), generate an HTML report,\n * and return the file path. Resets the store after reporting (clean slate\n * for next cycle).\n *\n * Returns the absolute path to the generated HTML file, or empty string\n * if no samples were collected.\n */\nexport async function report(): Promise<string> {\n if (!profiling || !session) {\n console.log('no samples collected');\n return '';\n }\n\n const { profile } = await session.post('Profiler.stop');\n await session.post('Profiler.disable');\n profiling = false;\n\n processProfile(profile);\n\n let filepath = '';\n\n if (store.packages.size > 0) {\n filepath = generateReport(store);\n } else {\n console.log('no samples collected');\n }\n\n store.clear();\n return filepath;\n}\n\n/**\n * Process a V8 CPUProfile: walk each sample, parse the frame, resolve\n * the package, and record into the store. Uses timeDeltas for wall-time\n * microsecond accumulation.\n */\nfunction processProfile(profile: Profiler.Profile): void {\n const nodeMap = new Map(profile.nodes.map((n) => [n.id, n]));\n const samples = profile.samples ?? [];\n const timeDeltas = profile.timeDeltas ?? [];\n\n for (let i = 0; i < samples.length; i++) {\n const node = nodeMap.get(samples[i]!);\n if (!node) continue;\n\n const deltaUs = timeDeltas[i] ?? 0;\n const parsed = parseFrame(node.callFrame as RawCallFrame);\n\n if (parsed.kind === 'user') {\n if (parsed.filePath.startsWith('node:')) {\n // Node.js built-in: attribute to \"node (built-in)\" package\n const relativePath = parsed.filePath.slice(5);\n store.record('node (built-in)', relativePath, parsed.functionId, deltaUs);\n } else {\n const { packageName, relativePath } = resolver.resolve(parsed.filePath);\n store.record(packageName, relativePath, parsed.functionId, deltaUs);\n }\n } else {\n store.recordInternal(deltaUs);\n }\n }\n}\n\n/** @internal -- exposed for testing only */\nexport function _getStore(): SampleStore {\n return store;\n}\n"],"mappings":";;;;;;;;;;;;;;AAWA,SAAgB,WAAW,OAAkC;CAC3D,MAAM,EAAE,KAAK,cAAc,eAAe;AAG1C,KAAI,QAAQ,GACV,QAAO,EAAE,MAAM,YAAY;AAI7B,KAAI,IAAI,WAAW,QAAQ,CAEzB,QAAO;EAAE,MAAM;EAAQ,UAAU;EAAK,YADnB,GAAG,gBAAgB,cAAc,GAAG,aAAa;EAClB;AAIpD,KAAI,IAAI,WAAW,QAAQ,CACzB,QAAO,EAAE,MAAM,QAAQ;AAIzB,KAAI,IAAI,SAAS,OAAO,CACtB,QAAO,EAAE,MAAM,QAAQ;AAWzB,QAAO;EAAE,MAAM;EAAQ,UAPN,IAAI,WAAW,UAAU,GACtC,cAAc,IAAI,GAClB;EAK6B,YAFd,GAAG,gBAAgB,cAAc,GAAG,aAAa;EAEvB;;;;;;;;;;;;;;AC/B/C,IAAa,kBAAb,MAA6B;CAC3B,AAAiB;CACjB,AAAiB,mCAAmB,IAAI,KAA4B;CAEpE,YAAY,aAAqB;AAC/B,OAAK,cAAc;;CAGrB,QAAQ,kBAAyE;EAC/E,MAAM,iBAAiB,GAAG,IAAI,cAAc;EAC5C,MAAM,UAAU,iBAAiB,YAAY,eAAe;AAE5D,MAAI,YAAY,IAAI;GAGlB,MAAM,WADe,iBAAiB,UAAU,UAAU,eAAe,OAAO,CAClD,MAAM,IAAI;GAExC,IAAI;GACJ,IAAI;AAEJ,OAAI,SAAS,GAAI,WAAW,IAAI,EAAE;AAEhC,kBAAc,GAAG,SAAS,GAAG,GAAG,SAAS;AACzC,mBAAe;UACV;AACL,kBAAc,SAAS;AACvB,mBAAe;;GAGjB,MAAM,eAAe,SAAS,MAAM,aAAa,CAAC,KAAK,IAAI;AAE3D,UAAO;IAAE;IAAa;IAAc;;AAStC,SAAO;GAAE,aALW,KAAK,gBAAgB,iBAAiB;GAKpC,cAJD,SAAS,KAAK,aAAa,iBAAiB,CAC9D,MAAM,IAAI,CACV,KAAK,IAAI;GAEwB;;;;;;CAOtC,AAAQ,gBAAgB,kBAAkC;EACxD,IAAI,MAAM,QAAQ,iBAAiB;AAEnC,SAAO,MAAM;GACX,MAAM,SAAS,KAAK,iBAAiB,IAAI,IAAI;AAC7C,OAAI,WAAW,QACb;QAAI,WAAW,KACb,QAAO;SAIT,KAAI;IACF,MAAM,MAAM,aAAa,KAAK,KAAK,eAAe,EAAE,QAAQ;IAE5D,MAAM,OADM,KAAK,MAAM,IAAI,CACV,QAAQ;AACzB,SAAK,iBAAiB,IAAI,KAAK,KAAK;AACpC,QAAI,SAAS,KACX,QAAO;WAEH;AAEN,SAAK,iBAAiB,IAAI,KAAK,KAAK;;GAIxC,MAAM,SAAS,QAAQ,IAAI;AAC3B,OAAI,WAAW,IAEb,QAAO;AAET,SAAM;;;;;;;ACxEZ,MAAM,gBAAgB;;;;;;;;AAStB,SAAgB,UAAU,OAAoB,aAAiC;CAE7E,IAAI,cAAc;AAClB,MAAK,MAAM,WAAW,MAAM,SAAS,QAAQ,CAC3C,MAAK,MAAM,WAAW,QAAQ,QAAQ,CACpC,MAAK,MAAM,MAAM,QAAQ,QAAQ,CAC/B,gBAAe;AAKrB,KAAI,gBAAgB,EAClB,QAAO;EACL,4BAAW,IAAI,MAAM,EAAC,gBAAgB;EACtC,aAAa;EACb,UAAU,EAAE;EACZ,YAAY;EACZ;EACD;CAGH,MAAM,YAAY,cAAc;CAChC,MAAM,WAA2B,EAAE;CACnC,IAAI,qBAAqB;AAGzB,MAAK,MAAM,CAAC,aAAa,YAAY,MAAM,UAAU;EAEnD,IAAI,gBAAgB;AACpB,OAAK,MAAM,WAAW,QAAQ,QAAQ,CACpC,MAAK,MAAM,MAAM,QAAQ,QAAQ,CAC/B,kBAAiB;AAKrB,MAAI,gBAAgB,WAAW;AAC7B;AACA;;EAIF,IAAI,qBAAqB;EACzB,MAAM,eAAe,MAAM,sBAAsB,IAAI,YAAY;AACjE,MAAI,aACF,MAAK,MAAM,gBAAgB,aAAa,QAAQ,CAC9C,MAAK,MAAM,SAAS,aAAa,QAAQ,CACvC,uBAAsB;EAM5B,MAAM,QAAqB,EAAE;EAC7B,IAAI,iBAAiB;AAErB,OAAK,MAAM,CAAC,UAAU,YAAY,SAAS;GAEzC,IAAI,aAAa;AACjB,QAAK,MAAM,MAAM,QAAQ,QAAQ,CAC/B,eAAc;AAIhB,OAAI,aAAa,WAAW;AAC1B;AACA;;GAIF,IAAI,kBAAkB;GACtB,MAAM,eAAe,cAAc,IAAI,SAAS;AAChD,OAAI,aACF,MAAK,MAAM,SAAS,aAAa,QAAQ,CACvC,oBAAmB;GAKvB,MAAM,YAA6B,EAAE;GACrC,IAAI,iBAAiB;AAErB,QAAK,MAAM,CAAC,UAAU,eAAe,SAAS;AAE5C,QAAI,aAAa,WAAW;AAC1B;AACA;;IAGF,MAAM,kBAAkB,cAAc,IAAI,SAAS,IAAI;AAEvD,cAAU,KAAK;KACb,MAAM;KACN,QAAQ;KACR,KAAM,aAAa,cAAe;KAClC,aAAa;KACd,CAAC;;AAIJ,aAAU,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;AAE7C,SAAM,KAAK;IACT,MAAM;IACN,QAAQ;IACR,KAAM,aAAa,cAAe;IAClC,aAAa;IACb;IACA,YAAY;IACb,CAAC;;AAIJ,QAAM,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;AAEzC,WAAS,KAAK;GACZ,MAAM;GACN,QAAQ;GACR,KAAM,gBAAgB,cAAe;GACrC,cAAc,gBAAgB;GAC9B,aAAa;GACb;GACA,YAAY;GACb,CAAC;;AAIJ,UAAS,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;AAE5C,QAAO;EACL,4BAAW,IAAI,MAAM,EAAC,gBAAgB;EACtC;EACA;EACA,YAAY;EACZ;EACD;;;;;;;;;;;;;;;;;ACpJH,SAAgB,WAAW,IAAoB;AAC7C,KAAI,OAAO,EAAG,QAAO;CAErB,MAAM,KAAK,KAAK;AAEhB,KAAI,MAAM,IAER,QAAO,IADS,KAAK,KACH,QAAQ,EAAE,CAAC;CAG/B,MAAM,UAAU,KAAK,MAAM,GAAG;AAC9B,QAAO,GAAG,UAAU,IAAI,IAAI,QAAQ;;;;;;AAOtC,SAAgB,UAAU,IAAY,SAAyB;AAC7D,KAAI,YAAY,EAAG,QAAO;AAC1B,QAAO,IAAK,KAAK,UAAW,KAAK,QAAQ,EAAE,CAAC;;;;;;;AAQ9C,SAAgB,WAAW,KAAqB;AAC9C,QAAO,IACJ,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,SAAS,CACvB,QAAQ,MAAM,QAAQ;;;;;ACrC3B,SAAS,cAAsB;AAC7B,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsOT,SAAS,mBACP,UACA,YACA,aACQ;CACR,IAAI,OAAO;AAEX,MAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,MAAM,IAAI,eAAe,gBAAgB;EAC/C,MAAM,SAAS,cAAc,IAAK,IAAI,SAAS,cAAe,MAAM;AACpE,UAAQ;mBACO,IAAI;+BACQ,WAAW,IAAI,KAAK,CAAC;8BACtB,WAAW,WAAW,IAAI,OAAO,CAAC,CAAC;;;wEAGO,OAAO,QAAQ,EAAE,CAAC;oCACtD,WAAW,UAAU,IAAI,QAAQ,YAAY,CAAC,CAAC;;;8BAGrD,IAAI,YAAY;;;AAI5C,KAAI,aAAa,EACf,SAAQ;;sCAE0B,WAAW;;;;;AAO/C,QAAO;;;;;;;;;;eAUM,KAAK;;;;AAKpB,SAAS,WACP,UACA,YACA,aACQ;CACR,IAAI,OAAO;AAEX,MAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,QAAQ,IAAI,eAAe,YAAY;AAC7C,UAAQ,0BAA0B,MAAM;AACxC,UAAQ;AACR,UAAQ;AACR,UAAQ,2BAA2B,WAAW,IAAI,KAAK,CAAC;AACxD,UAAQ,4BAA4B,WAAW,WAAW,IAAI,OAAO,CAAC,CAAC,YAAY,WAAW,UAAU,IAAI,QAAQ,YAAY,CAAC,CAAC,YAAY,IAAI,YAAY;AAC9J,UAAQ;AAER,OAAK,MAAM,QAAQ,IAAI,OAAO;AAC5B,WAAQ;AACR,WAAQ;AACR,WAAQ;AACR,WAAQ,2BAA2B,WAAW,KAAK,KAAK,CAAC;AACzD,WAAQ,4BAA4B,WAAW,WAAW,KAAK,OAAO,CAAC,CAAC,YAAY,WAAW,UAAU,KAAK,QAAQ,YAAY,CAAC,CAAC,YAAY,KAAK,YAAY;AACjK,WAAQ;AAER,QAAK,MAAM,MAAM,KAAK,WAAW;AAC/B,YAAQ;AACR,YAAQ;AACR,YAAQ,2BAA2B,WAAW,GAAG,KAAK,CAAC;AACvD,YAAQ,6BAA6B,WAAW,WAAW,GAAG,OAAO,CAAC,CAAC,YAAY,WAAW,UAAU,GAAG,QAAQ,YAAY,CAAC,CAAC,YAAY,GAAG,YAAY;AAC5J,YAAQ;;AAGV,OAAI,KAAK,aAAa,EACpB,SAAQ,2CAA2C,KAAK,WAAW;AAGrE,WAAQ;;AAGV,MAAI,IAAI,aAAa,EACnB,SAAQ,2CAA2C,IAAI,WAAW;AAGpE,UAAQ;;AAGV,KAAI,aAAa,EACf,SAAQ,kCAAkC,WAAW;AAGvD,SAAQ;AACR,QAAO;;;;;AAMT,SAAgB,WAAW,MAA0B;CACnD,MAAM,eAAe,mBAAmB,KAAK,UAAU,KAAK,YAAY,KAAK,YAAY;CACzF,MAAM,OAAO,WAAW,KAAK,UAAU,KAAK,YAAY,KAAK,YAAY;CACzE,MAAM,iBAAiB,WAAW,WAAW,KAAK,YAAY,CAAC;CAE/D,MAAM,YAAY,WAAW,KAAK,YAAY;AAE9C,QAAO;;;;;WAKE,UAAU;WACV,aAAa,CAAC;;;;QAIjB,UAAU;gCACc,WAAW,KAAK,UAAU,CAAC,6BAA6B,eAAe;;;IAGnG,aAAa;;;IAGb,KAAK;;;;;;;;;;;;;ACtWT,SAAS,mBAA2B;CAClC,MAAM,sBAAM,IAAI,MAAM;CACtB,MAAM,OAAO,MAAc,OAAO,EAAE,CAAC,SAAS,GAAG,IAAI;AAGrD,QAAO,gBAFM,GAAG,IAAI,aAAa,CAAC,GAAG,IAAI,IAAI,UAAU,GAAG,EAAE,CAAC,GAAG,IAAI,IAAI,SAAS,CAAC,GAEtD,GADf,GAAG,IAAI,IAAI,UAAU,CAAC,GAAG,IAAI,IAAI,YAAY,CAAC,GAAG,IAAI,IAAI,YAAY,CAAC,GAC/C;;AAGtC,SAAS,gBAAgB,KAAqB;AAC5C,KAAI;EACF,MAAM,MAAM,aAAa,KAAK,KAAK,eAAe,EAAE,QAAQ;AAE5D,SADY,KAAK,MAAM,IAAI,CAChB,QAAQ;SACb;AACN,SAAO;;;AAIX,SAAgB,eAAe,OAAoB,KAAsB;CACvE,MAAM,cAAc,OAAO,QAAQ,KAAK;CAGxC,MAAM,OAAO,WADA,UAAU,OADH,gBAAgB,YAAY,CACN,CACb;CAC7B,MAAM,WAAW,kBAAkB;CACnC,MAAM,WAAW,KAAK,aAAa,SAAS;AAC5C,eAAc,UAAU,MAAM,QAAQ;AACtC,SAAQ,IAAI,uBAAuB,WAAW;AAC9C,QAAO;;;;;;;;;;;;;;;AC9BT,IAAa,cAAb,MAAyB;CACvB,AAAQ,uBAAO,IAAI,KAA+C;CAClE,AAAQ,yBAAS,IAAI,KAA+C;CACpE,AAAQ,gBAAgB;CACxB,AAAQ,kBAAkB;;;;;;CAO1B,OAAO,aAAqB,cAAsB,YAAoB,SAAuB;EAE3F,IAAI,UAAU,KAAK,KAAK,IAAI,YAAY;AACxC,MAAI,YAAY,QAAW;AACzB,6BAAU,IAAI,KAAkC;AAChD,QAAK,KAAK,IAAI,aAAa,QAAQ;;EAGrC,IAAI,UAAU,QAAQ,IAAI,aAAa;AACvC,MAAI,YAAY,QAAW;AACzB,6BAAU,IAAI,KAAqB;AACnC,WAAQ,IAAI,cAAc,QAAQ;;AAGpC,UAAQ,IAAI,aAAa,QAAQ,IAAI,WAAW,IAAI,KAAK,QAAQ;EAGjE,IAAI,eAAe,KAAK,OAAO,IAAI,YAAY;AAC/C,MAAI,iBAAiB,QAAW;AAC9B,kCAAe,IAAI,KAAkC;AACrD,QAAK,OAAO,IAAI,aAAa,aAAa;;EAG5C,IAAI,eAAe,aAAa,IAAI,aAAa;AACjD,MAAI,iBAAiB,QAAW;AAC9B,kCAAe,IAAI,KAAqB;AACxC,gBAAa,IAAI,cAAc,aAAa;;AAG9C,eAAa,IAAI,aAAa,aAAa,IAAI,WAAW,IAAI,KAAK,EAAE;;;CAIvE,eAAe,SAAuB;AACpC,OAAK,iBAAiB;AACtB,OAAK,mBAAmB;;;CAI1B,QAAc;AACZ,OAAK,uBAAO,IAAI,KAA+C;AAC/D,OAAK,yBAAS,IAAI,KAA+C;AACjE,OAAK,gBAAgB;AACrB,OAAK,kBAAkB;;;CAIzB,IAAI,WAAkE;AACpE,SAAO,KAAK;;;CAId,IAAI,WAAmB;AACrB,SAAO,KAAK;;;CAId,IAAI,wBAA+E;AACjF,SAAO,KAAK;;;CAId,IAAI,sBAA8B;AAChC,SAAO,KAAK;;;;;;AC3EhB,IAAI,UAA0B;AAC9B,IAAI,YAAY;AAChB,MAAM,QAAQ,IAAI,aAAa;AAC/B,MAAM,WAAW,IAAI,gBAAgB,QAAQ,KAAK,CAAC;;;;AAKnD,eAAsB,MAAM,SAAgD;AAC1E,KAAI,UAAW;AAEf,KAAI,YAAY,MAAM;AACpB,YAAU,IAAI,SAAS;AACvB,UAAQ,SAAS;;AAGnB,OAAM,QAAQ,KAAK,kBAAkB;AAErC,KAAI,SAAS,aAAa,OACxB,OAAM,QAAQ,KAAK,gCAAgC,EACjD,UAAU,QAAQ,UACnB,CAAC;AAGJ,OAAM,QAAQ,KAAK,iBAAiB;AACpC,aAAY;;;;;AAMd,eAAsB,QAAuB;AAC3C,KAAI,aAAa,SAAS;AACxB,QAAM,QAAQ,KAAK,gBAAgB;AACnC,QAAM,QAAQ,KAAK,mBAAmB;AACtC,cAAY;;AAEd,OAAM,OAAO;;;;;;;;;;;AAYf,eAAsB,SAA0B;AAC9C,KAAI,CAAC,aAAa,CAAC,SAAS;AAC1B,UAAQ,IAAI,uBAAuB;AACnC,SAAO;;CAGT,MAAM,EAAE,YAAY,MAAM,QAAQ,KAAK,gBAAgB;AACvD,OAAM,QAAQ,KAAK,mBAAmB;AACtC,aAAY;AAEZ,gBAAe,QAAQ;CAEvB,IAAI,WAAW;AAEf,KAAI,MAAM,SAAS,OAAO,EACxB,YAAW,eAAe,MAAM;KAEhC,SAAQ,IAAI,uBAAuB;AAGrC,OAAM,OAAO;AACb,QAAO;;;;;;;AAQT,SAAS,eAAe,SAAiC;CACvD,MAAM,UAAU,IAAI,IAAI,QAAQ,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;CAC5D,MAAM,UAAU,QAAQ,WAAW,EAAE;CACrC,MAAM,aAAa,QAAQ,cAAc,EAAE;AAE3C,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,OAAO,QAAQ,IAAI,QAAQ,GAAI;AACrC,MAAI,CAAC,KAAM;EAEX,MAAM,UAAU,WAAW,MAAM;EACjC,MAAM,SAAS,WAAW,KAAK,UAA0B;AAEzD,MAAI,OAAO,SAAS,OAClB,KAAI,OAAO,SAAS,WAAW,QAAQ,EAAE;GAEvC,MAAM,eAAe,OAAO,SAAS,MAAM,EAAE;AAC7C,SAAM,OAAO,mBAAmB,cAAc,OAAO,YAAY,QAAQ;SACpE;GACL,MAAM,EAAE,aAAa,iBAAiB,SAAS,QAAQ,OAAO,SAAS;AACvE,SAAM,OAAO,aAAa,cAAc,OAAO,YAAY,QAAQ;;MAGrE,OAAM,eAAe,QAAQ"}
|
package/package.json
CHANGED
|
@@ -41,6 +41,7 @@ export function aggregate(store: SampleStore, projectName: string): ReportData {
|
|
|
41
41
|
totalTimeUs: 0,
|
|
42
42
|
packages: [],
|
|
43
43
|
otherCount: 0,
|
|
44
|
+
projectName,
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -157,5 +158,6 @@ export function aggregate(store: SampleStore, projectName: string): ReportData {
|
|
|
157
158
|
totalTimeUs,
|
|
158
159
|
packages,
|
|
159
160
|
otherCount: topLevelOtherCount,
|
|
161
|
+
projectName,
|
|
160
162
|
};
|
|
161
163
|
}
|
package/src/reporter/html.ts
CHANGED
|
@@ -350,17 +350,19 @@ export function renderHtml(data: ReportData): string {
|
|
|
350
350
|
const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs);
|
|
351
351
|
const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));
|
|
352
352
|
|
|
353
|
+
const titleName = escapeHtml(data.projectName);
|
|
354
|
+
|
|
353
355
|
return `<!DOCTYPE html>
|
|
354
356
|
<html lang="en">
|
|
355
357
|
<head>
|
|
356
358
|
<meta charset="utf-8">
|
|
357
359
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
358
|
-
<title
|
|
360
|
+
<title>${titleName} · where-you-at report</title>
|
|
359
361
|
<style>${generateCss()}
|
|
360
362
|
</style>
|
|
361
363
|
</head>
|
|
362
364
|
<body>
|
|
363
|
-
<h1
|
|
365
|
+
<h1>${titleName}</h1>
|
|
364
366
|
<div class="meta">Generated ${escapeHtml(data.timestamp)} · Total wall time: ${totalFormatted}</div>
|
|
365
367
|
|
|
366
368
|
<h2>Summary</h2>
|