@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 CHANGED
@@ -1,91 +1,63 @@
1
1
  <p align="center">
2
- <svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120" fill="none">
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
- Zero-dependency sampling profiler that shows which npm packages consume your wall time.
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
- ## The Problem
26
-
27
- You have a slow Node.js process -- maybe a test suite that takes too long, a server that's sluggish to start, or a CLI tool that lags. You fire up a profiler and get a wall of individual function timings. You can see *what* is slow, but not *where* the time is going at the package level.
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
- ```bash
36
- npm install @mtharrison/pkg-profiler
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
- ## Usage
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
- The generated HTML report shows a breakdown of wall time by package, with expandable trees to drill down into individual files and functions.
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
- Starts the V8 CPU sampling profiler. If already profiling, this is a safe no-op.
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 (optional) |
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
- Stops the profiler, processes collected samples, generates an HTML report, and returns the absolute path to the report file. Resets all accumulated data after reporting (clean slate for the next cycle).
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
- Stops the profiler (if running) and resets all accumulated sample data without generating a report.
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
- The library uses the V8 CPU profiler (via `node:inspector`) to periodically sample the call stack. Each sample's leaf frame (the function currently executing) is attributed the elapsed wall time. File paths are resolved to npm packages by detecting `node_modules` segments, giving you a per-package time breakdown without any code instrumentation.
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
- - Node.js >= 20.0.0
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>where-you-at report</title>
547
+ <title>${titleName} · where-you-at report</title>
545
548
  <style>${generateCss()}
546
549
  </style>
547
550
  </head>
548
551
  <body>
549
- <h1>where-you-at</h1>
552
+ <h1>${titleName}</h1>
550
553
  <div class="meta">Generated ${escapeHtml(data.timestamp)} &middot; Total wall time: ${totalFormatted}</div>
551
554
 
552
555
  <h2>Summary</h2>
@@ -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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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))} &middot; ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} &middot; ${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))} &middot; ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} &middot; ${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))} &middot; ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} &middot; ${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)} &middot; 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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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))} &middot; ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} &middot; ${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))} &middot; ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} &middot; ${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))} &middot; ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} &middot; ${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)} &middot; 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>where-you-at report</title>
546
+ <title>${titleName} · where-you-at report</title>
544
547
  <style>${generateCss()}
545
548
  </style>
546
549
  </head>
547
550
  <body>
548
- <h1>where-you-at</h1>
551
+ <h1>${titleName}</h1>
549
552
  <div class="meta">Generated ${escapeHtml(data.timestamp)} &middot; 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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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))} &middot; ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} &middot; ${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))} &middot; ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} &middot; ${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))} &middot; ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} &middot; ${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)} &middot; 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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\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))} &middot; ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} &middot; ${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))} &middot; ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} &middot; ${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))} &middot; ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} &middot; ${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)} &middot; 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mtharrison/pkg-profiler",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Zero-dependency sampling profiler that shows which npm packages consume your wall time",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -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
  }
@@ -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>where-you-at report</title>
360
+ <title>${titleName} · where-you-at report</title>
359
361
  <style>${generateCss()}
360
362
  </style>
361
363
  </head>
362
364
  <body>
363
- <h1>where-you-at</h1>
365
+ <h1>${titleName}</h1>
364
366
  <div class="meta">Generated ${escapeHtml(data.timestamp)} &middot; Total wall time: ${totalFormatted}</div>
365
367
 
366
368
  <h2>Summary</h2>
package/src/types.ts CHANGED
@@ -47,4 +47,5 @@ export interface ReportData {
47
47
  totalTimeUs: number;
48
48
  packages: PackageEntry[];
49
49
  otherCount: number;
50
+ projectName: string;
50
51
  }