@mtharrison/pkg-profiler 2.0.0 → 2.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/dist/index.cjs +250 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +250 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/async-tracker.ts +128 -17
- package/src/pkg-profile.ts +4 -0
- package/src/reporter/aggregate.ts +10 -3
- package/src/reporter/html.ts +136 -26
- package/src/sampler.ts +11 -0
- package/src/types.ts +1 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/async-tracker.ts","../src/frame-parser.ts","../src/package-resolver.ts","../src/reporter/format.ts","../src/reporter/html.ts","../src/pkg-profile.ts","../src/reporter/aggregate.ts","../src/sample-store.ts","../src/sampler.ts"],"sourcesContent":["/**\n * Opt-in async I/O wait time tracker using node:async_hooks.\n *\n * Tracks the time between async resource init (when the I/O op is started)\n * and the first before callback (when the callback fires), attributing\n * that wait time to the package/file/function that initiated the operation.\n */\n\nimport { createHook } from 'node:async_hooks';\nimport type { AsyncHook } from 'node:async_hooks';\nimport type { PackageResolver } from './package-resolver.js';\nimport type { SampleStore } from './sample-store.js';\n\n/** Async resource types worth tracking — I/O and timers, not promises. */\nconst TRACKED_TYPES = new Set([\n 'TCPCONNECTWRAP',\n 'TCPWRAP',\n 'PIPEWRAP',\n 'PIPECONNECTWRAP',\n 'TLSWRAP',\n 'FSREQCALLBACK',\n 'FSREQPROMISE',\n 'GETADDRINFOREQWRAP',\n 'GETNAMEINFOREQWRAP',\n 'HTTPCLIENTREQUEST',\n 'HTTPINCOMINGMESSAGE',\n 'SHUTDOWNWRAP',\n 'WRITEWRAP',\n 'ZLIB',\n 'Timeout',\n]);\n\ninterface PendingOp {\n startHrtime: [number, number];\n pkg: string;\n file: string;\n fn: string;\n}\n\n/**\n * Parse a single line from an Error().stack trace into file path and function id.\n * Returns null for lines that don't match V8's stack frame format or are node internals.\n *\n * Handles these V8 formats:\n * \" at functionName (/absolute/path:line:col)\"\n * \" at /absolute/path:line:col\"\n * \" at Object.functionName (/absolute/path:line:col)\"\n */\nexport function parseStackLine(line: string): { filePath: string; functionId: string } | null {\n // Match \" at [funcName] (filePath:line:col)\" or \" at filePath:line:col\"\n const match = line.match(/^\\s+at\\s+(?:(.+?)\\s+\\()?(.+?):(\\d+):\\d+\\)?$/);\n if (!match) return null;\n\n const rawFn = match[1] ?? '';\n const filePath = match[2]!;\n const lineNum = match[3]!;\n\n // Skip node internals (node:xxx, <anonymous>, etc)\n if (filePath.startsWith('node:') || filePath.startsWith('<')) return null;\n\n // Use last segment of function name (strip \"Object.\" etc)\n const fnParts = rawFn.split('.');\n const fnName = fnParts[fnParts.length - 1] || '<anonymous>';\n const functionId = `${fnName}:${lineNum}`;\n\n return { filePath, functionId };\n}\n\nexport class AsyncTracker {\n private readonly resolver: PackageResolver;\n private readonly store: SampleStore;\n private readonly thresholdUs: number;\n private hook: AsyncHook | null = null;\n private pending = new Map<number, PendingOp>();\n\n /**\n * @param resolver - PackageResolver for mapping file paths to packages\n * @param store - SampleStore to record async wait times into\n * @param thresholdUs - Minimum wait duration in microseconds to record (default 1000 = 1ms)\n */\n constructor(resolver: PackageResolver, store: SampleStore, thresholdUs: number = 1000) {\n this.resolver = resolver;\n this.store = store;\n this.thresholdUs = thresholdUs;\n }\n\n enable(): void {\n if (this.hook) return;\n\n this.hook = createHook({\n init: (asyncId: number, type: string) => {\n if (!TRACKED_TYPES.has(type)) return;\n\n // Capture stack trace with limited depth\n const holder: { stack?: string } = {};\n const origLimit = Error.stackTraceLimit;\n Error.stackTraceLimit = 8;\n Error.captureStackTrace(holder);\n Error.stackTraceLimit = origLimit;\n\n const stack = holder.stack;\n if (!stack) return;\n\n // Find the first user-code frame (skip async_hooks internals)\n const lines = stack.split('\\n');\n let parsed: { filePath: string; functionId: string } | null = null;\n for (let i = 1; i < lines.length; i++) {\n const result = parseStackLine(lines[i]!);\n if (result) {\n // Skip frames inside this module\n if (result.filePath.includes('async-tracker')) continue;\n parsed = result;\n break;\n }\n }\n\n if (!parsed) return;\n\n // Resolve to package\n const { packageName, relativePath } = this.resolver.resolve(parsed.filePath);\n\n this.pending.set(asyncId, {\n startHrtime: process.hrtime(),\n pkg: packageName,\n file: relativePath,\n fn: parsed.functionId,\n });\n },\n\n before: (asyncId: number) => {\n const op = this.pending.get(asyncId);\n if (!op) return;\n\n const elapsed = process.hrtime(op.startHrtime);\n const durationUs = elapsed[0] * 1_000_000 + Math.round(elapsed[1] / 1000);\n\n if (durationUs >= this.thresholdUs) {\n this.store.record(op.pkg, op.file, op.fn, durationUs);\n }\n\n this.pending.delete(asyncId);\n },\n\n destroy: (asyncId: number) => {\n // Clean up ops that never got a before callback (aborted)\n this.pending.delete(asyncId);\n },\n });\n\n this.hook.enable();\n }\n\n disable(): void {\n if (!this.hook) return;\n\n this.hook.disable();\n\n // Resolve any pending ops using current time\n const now = process.hrtime();\n for (const [, op] of this.pending) {\n // Compute elapsed from op start to now\n let secs = now[0] - op.startHrtime[0];\n let nanos = now[1] - op.startHrtime[1];\n if (nanos < 0) {\n secs -= 1;\n nanos += 1_000_000_000;\n }\n const durationUs = secs * 1_000_000 + Math.round(nanos / 1000);\n\n if (durationUs >= this.thresholdUs) {\n this.store.record(op.pkg, op.file, op.fn, durationUs);\n }\n }\n\n this.pending.clear();\n this.hook = null;\n }\n}\n","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 *\n * @param frame - Raw call frame from the V8 CPU profiler.\n * @returns A classified frame: `'user'` with file path and function id, or a non-user kind.\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 * 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 *\n * @param us - Time value in microseconds.\n * @returns Human-readable time string.\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 *\n * @param us - Time value in microseconds.\n * @param totalUs - Total time in microseconds (denominator).\n * @returns Percentage string like `\"12.3%\"`.\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 *\n * @param str - Raw string to escape.\n * @returns HTML-safe string.\n */\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","/**\n * HTML renderer for the profiling report.\n *\n * Generates a self-contained HTML file (inline CSS/JS, no external dependencies)\n * with a summary table, expandable Package > File > Function tree, and an\n * interactive threshold slider that filters data client-side.\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 --bar-fill-async: #f5943b;\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 /* Threshold slider */\n .threshold-control {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-bottom: 1rem;\n font-size: 0.85rem;\n }\n\n .threshold-control label {\n font-weight: 600;\n color: var(--muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n font-size: 0.8rem;\n }\n\n .threshold-control input[type=\"range\"] {\n flex: 1;\n max-width: 240px;\n height: 8px;\n appearance: none;\n -webkit-appearance: none;\n background: var(--bar-track);\n border-radius: 4px;\n outline: none;\n }\n\n .threshold-control input[type=\"range\"]::-webkit-slider-thumb {\n appearance: none;\n -webkit-appearance: none;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--bar-fill);\n cursor: pointer;\n }\n\n .threshold-control input[type=\"range\"]::-moz-range-thumb {\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--bar-fill);\n cursor: pointer;\n border: none;\n }\n\n .threshold-control span {\n font-family: var(--font-mono);\n font-size: 0.85rem;\n min-width: 3.5em;\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 td.async-col { color: var(--bar-fill-async); }\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 .tree-async {\n font-family: var(--font-mono);\n font-size: 0.8rem;\n color: var(--bar-fill-async);\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 generateJs(): string {\n return `\n(function() {\n var DATA = window.__REPORT_DATA__;\n if (!DATA) return;\n var HAS_ASYNC = !!(DATA.totalAsyncTimeUs && DATA.totalAsyncTimeUs > 0);\n\n function formatTime(us) {\n if (us === 0) return '0ms';\n var ms = us / 1000;\n if (ms >= 1000) return (ms / 1000).toFixed(2) + 's';\n var rounded = Math.round(ms);\n return (rounded < 1 ? 1 : rounded) + 'ms';\n }\n\n function formatPct(us, totalUs) {\n if (totalUs === 0) return '0.0%';\n return ((us / totalUs) * 100).toFixed(1) + '%';\n }\n\n function escapeHtml(str) {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n }\n\n function applyThreshold(data, pct) {\n var threshold = data.totalTimeUs * (pct / 100);\n var filtered = [];\n var otherCount = 0;\n\n for (var i = 0; i < data.packages.length; i++) {\n var pkg = data.packages[i];\n if (pkg.timeUs < threshold) {\n otherCount++;\n continue;\n }\n\n var files = [];\n var fileOtherCount = 0;\n\n for (var j = 0; j < pkg.files.length; j++) {\n var file = pkg.files[j];\n if (file.timeUs < threshold) {\n fileOtherCount++;\n continue;\n }\n\n var functions = [];\n var funcOtherCount = 0;\n\n for (var k = 0; k < file.functions.length; k++) {\n var fn = file.functions[k];\n if (fn.timeUs < threshold) {\n funcOtherCount++;\n continue;\n }\n functions.push(fn);\n }\n\n files.push({\n name: file.name,\n timeUs: file.timeUs,\n pct: file.pct,\n sampleCount: file.sampleCount,\n asyncTimeUs: file.asyncTimeUs,\n asyncPct: file.asyncPct,\n asyncOpCount: file.asyncOpCount,\n functions: functions,\n otherCount: funcOtherCount\n });\n }\n\n filtered.push({\n name: pkg.name,\n timeUs: pkg.timeUs,\n pct: pkg.pct,\n isFirstParty: pkg.isFirstParty,\n sampleCount: pkg.sampleCount,\n asyncTimeUs: pkg.asyncTimeUs,\n asyncPct: pkg.asyncPct,\n asyncOpCount: pkg.asyncOpCount,\n files: files,\n otherCount: fileOtherCount\n });\n }\n\n return { packages: filtered, otherCount: otherCount };\n }\n\n function renderTable(packages, otherCount, totalTimeUs) {\n var rows = '';\n for (var i = 0; i < packages.length; i++) {\n var pkg = packages[i];\n var cls = pkg.isFirstParty ? 'first-party' : 'dependency';\n var pctVal = totalTimeUs > 0 ? (pkg.timeUs / totalTimeUs) * 100 : 0;\n rows += '<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\"><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></td>' +\n '<td class=\"numeric\">' + pkg.sampleCount + '</td>';\n if (HAS_ASYNC) {\n rows += '<td class=\"numeric async-col\">' + escapeHtml(formatTime(pkg.asyncTimeUs || 0)) + '</td>' +\n '<td class=\"numeric async-col\">' + (pkg.asyncOpCount || 0) + '</td>';\n }\n rows += '</tr>';\n }\n\n if (otherCount > 0) {\n rows += '<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 if (HAS_ASYNC) {\n rows += '<td class=\"numeric\"></td><td class=\"numeric\"></td>';\n }\n rows += '</tr>';\n }\n\n var headers = '<th>Package</th><th>Wall Time</th><th>% of Total</th><th>Samples</th>';\n if (HAS_ASYNC) {\n headers += '<th>Async Wait</th><th>Async Ops</th>';\n }\n\n return '<table><thead><tr>' + headers + '</tr></thead><tbody>' + rows + '</tbody></table>';\n }\n\n function asyncStats(entry) {\n if (!HAS_ASYNC) return '';\n var at = entry.asyncTimeUs || 0;\n var ac = entry.asyncOpCount || 0;\n if (at === 0 && ac === 0) return '';\n return ' <span class=\"tree-async\">| ' + escapeHtml(formatTime(at)) + ' async · ' + ac + ' ops</span>';\n }\n\n function renderTree(packages, otherCount, totalTimeUs) {\n var html = '<div class=\"tree\">';\n\n for (var i = 0; i < packages.length; i++) {\n var pkg = packages[i];\n var fpCls = pkg.isFirstParty ? ' fp-pkg' : '';\n html += '<details class=\"level-0' + fpCls + '\"><summary>';\n html += '<span class=\"tree-label pkg\">pkg</span>';\n html += '<span class=\"tree-name\">' + escapeHtml(pkg.name) + '</span>';\n html += '<span class=\"tree-stats\">' + escapeHtml(formatTime(pkg.timeUs)) + ' · ' + escapeHtml(formatPct(pkg.timeUs, totalTimeUs)) + ' · ' + pkg.sampleCount + ' samples</span>';\n html += asyncStats(pkg);\n html += '</summary>';\n\n for (var j = 0; j < pkg.files.length; j++) {\n var file = pkg.files[j];\n html += '<details class=\"level-1\"><summary>';\n html += '<span class=\"tree-label file\">file</span>';\n html += '<span class=\"tree-name\">' + escapeHtml(file.name) + '</span>';\n html += '<span class=\"tree-stats\">' + escapeHtml(formatTime(file.timeUs)) + ' · ' + escapeHtml(formatPct(file.timeUs, totalTimeUs)) + ' · ' + file.sampleCount + ' samples</span>';\n html += asyncStats(file);\n html += '</summary>';\n\n for (var k = 0; k < file.functions.length; k++) {\n var fn = file.functions[k];\n html += '<div class=\"level-2\">';\n html += '<span class=\"tree-label fn\">fn</span> ';\n html += '<span class=\"tree-name\">' + escapeHtml(fn.name) + '</span>';\n html += ' <span class=\"tree-stats\">' + escapeHtml(formatTime(fn.timeUs)) + ' · ' + escapeHtml(formatPct(fn.timeUs, totalTimeUs)) + ' · ' + fn.sampleCount + ' samples</span>';\n html += asyncStats(fn);\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 function update(pct) {\n var result = applyThreshold(DATA, pct);\n var summaryEl = document.getElementById('summary-container');\n var treeEl = document.getElementById('tree-container');\n if (summaryEl) summaryEl.innerHTML = renderTable(result.packages, result.otherCount, DATA.totalTimeUs);\n if (treeEl) treeEl.innerHTML = renderTree(result.packages, result.otherCount, DATA.totalTimeUs);\n }\n\n document.addEventListener('DOMContentLoaded', function() {\n update(5);\n var slider = document.getElementById('threshold-slider');\n var label = document.getElementById('threshold-value');\n if (slider) {\n slider.addEventListener('input', function() {\n var val = parseFloat(slider.value);\n if (label) label.textContent = val.toFixed(1) + '%';\n update(val);\n });\n }\n });\n})();\n`;\n}\n\nfunction renderSummaryTable(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n hasAsync: boolean,\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>${hasAsync ? `\n <td class=\"numeric async-col\">${escapeHtml(formatTime(pkg.asyncTimeUs ?? 0))}</td>\n <td class=\"numeric async-col\">${pkg.asyncOpCount ?? 0}</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>${hasAsync ? `\n <td class=\"numeric\"></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>${hasAsync ? `\n <th>Async Wait</th>\n <th>Async Ops</th>` : ''}\n </tr>\n </thead>\n <tbody>${rows}\n </tbody>\n </table>`;\n}\n\nfunction formatAsyncStats(entry: { asyncTimeUs?: number; asyncOpCount?: number }): string {\n const at = entry.asyncTimeUs ?? 0;\n const ac = entry.asyncOpCount ?? 0;\n if (at === 0 && ac === 0) return '';\n return ` <span class=\"tree-async\">| ${escapeHtml(formatTime(at))} async · ${ac} ops</span>`;\n}\n\nfunction renderTree(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n hasAsync: boolean,\n): string {\n let html = '<div class=\"tree\">';\n\n for (const pkg of packages) {\n const fpCls = pkg.isFirstParty ? ' fp-pkg' : '';\n html += `<details class=\"level-0${fpCls}\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label pkg\">pkg</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(pkg.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;\n if (hasAsync) html += formatAsyncStats(pkg);\n html += `</summary>`;\n\n for (const file of pkg.files) {\n html += `<details class=\"level-1\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label file\">file</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(file.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(file.timeUs))} · ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} · ${file.sampleCount} samples</span>`;\n if (hasAsync) html += formatAsyncStats(file);\n html += `</summary>`;\n\n for (const fn of file.functions) {\n html += `<div class=\"level-2\">`;\n html += `<span class=\"tree-label fn\">fn</span> `;\n html += `<span class=\"tree-name\">${escapeHtml(fn.name)}</span>`;\n html += ` <span class=\"tree-stats\">${escapeHtml(formatTime(fn.timeUs))} · ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} · ${fn.sampleCount} samples</span>`;\n if (hasAsync) html += formatAsyncStats(fn);\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 *\n * @param data - Aggregated report data (packages, timing, project name).\n * @returns A full HTML document string with inline CSS/JS and no external dependencies.\n */\nexport function renderHtml(data: ReportData): string {\n const hasAsync = !!(data.totalAsyncTimeUs && data.totalAsyncTimeUs > 0);\n const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs, hasAsync);\n const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs, hasAsync);\n const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));\n\n const titleName = escapeHtml(data.projectName);\n\n let metaLine = `Generated ${escapeHtml(data.timestamp)} · Total wall time: ${totalFormatted}`;\n if (hasAsync) {\n metaLine += ` · Total async wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs!))}`;\n }\n\n // Sanitize JSON for safe embedding in <script> — replace < to prevent </script> injection\n const safeJson = JSON.stringify(data).replace(/</g, '\\\\u003c');\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\">${metaLine}</div>\n\n <h2>Summary</h2>\n <div class=\"threshold-control\">\n <label>Threshold</label>\n <input type=\"range\" id=\"threshold-slider\" min=\"0\" max=\"20\" step=\"0.5\" value=\"5\">\n <span id=\"threshold-value\">5.0%</span>\n </div>\n <div id=\"summary-container\">${summaryTable}</div>\n\n <h2>Details</h2>\n <div id=\"tree-container\">${tree}</div>\n\n <script>var __REPORT_DATA__ = ${safeJson};</script>\n <script>${generateJs()}</script>\n</body>\n</html>`;\n}\n","/**\n * Immutable profiling result returned by `stop()` and `profile()`.\n *\n * Contains aggregated per-package timing data and a convenience method\n * to write a self-contained HTML report to disk.\n */\n\nimport { writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport type { PackageEntry, ReportData } from './types.js';\nimport { renderHtml } from './reporter/html.js';\n\nfunction generateFilename(timestamp: string): 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\nexport class PkgProfile {\n /** When the profile was captured */\n readonly timestamp: string;\n /** Total sampled wall time in microseconds */\n readonly totalTimeUs: number;\n /** Package breakdown sorted by time descending (all packages, no threshold applied) */\n readonly packages: PackageEntry[];\n /** Always 0 — threshold filtering is now applied client-side in the HTML report */\n readonly otherCount: number;\n /** Project name (from package.json) */\n readonly projectName: string;\n /** Total async wait time in microseconds (undefined when async tracking not enabled) */\n readonly totalAsyncTimeUs?: number;\n\n /** @internal */\n constructor(data: ReportData) {\n this.timestamp = data.timestamp;\n this.totalTimeUs = data.totalTimeUs;\n this.packages = data.packages;\n this.otherCount = data.otherCount;\n this.projectName = data.projectName;\n this.totalAsyncTimeUs = data.totalAsyncTimeUs;\n }\n\n /**\n * Write a self-contained HTML report to disk.\n *\n * @param path - Output file path. Defaults to `./where-you-at-{timestamp}.html` in cwd.\n * @returns Absolute path to the written file.\n */\n writeHtml(path?: string): string {\n const data: ReportData = {\n timestamp: this.timestamp,\n totalTimeUs: this.totalTimeUs,\n packages: this.packages,\n otherCount: this.otherCount,\n projectName: this.projectName,\n totalAsyncTimeUs: this.totalAsyncTimeUs,\n };\n const html = renderHtml(data);\n\n let filepath: string;\n if (path) {\n filepath = resolve(path);\n } else {\n const filename = generateFilename(this.timestamp);\n filepath = join(process.cwd(), filename);\n }\n\n writeFileSync(filepath, html, 'utf-8');\n return filepath;\n }\n}\n","/**\n * Transform raw SampleStore data into a sorted ReportData structure.\n *\n * Pure function: reads nested Maps from SampleStore, computes percentages,\n * and sorts by timeUs descending at each level. No threshold filtering —\n * all entries are included so the HTML report can apply thresholds client-side.\n */\n\nimport type { SampleStore } from '../sample-store.js';\nimport type {\n ReportData,\n PackageEntry,\n FileEntry,\n FunctionEntry,\n} from '../types.js';\n\n/**\n * Sum all microseconds in a SampleStore.\n */\nfunction sumStore(store: SampleStore): number {\n let total = 0;\n for (const fileMap of store.packages.values()) {\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n total += us;\n }\n }\n }\n return total;\n}\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 * @param asyncStore - Optional SampleStore with async wait time data\n * @returns ReportData with all packages sorted desc by time, no threshold applied\n */\nexport function aggregate(store: SampleStore, projectName: string, asyncStore?: SampleStore): ReportData {\n // 1. Calculate total user-attributed time\n const totalTimeUs = sumStore(store);\n const totalAsyncTimeUs = asyncStore ? sumStore(asyncStore) : 0;\n\n if (totalTimeUs === 0 && totalAsyncTimeUs === 0) {\n return {\n timestamp: new Date().toLocaleString(),\n totalTimeUs: 0,\n packages: [],\n otherCount: 0,\n projectName,\n };\n }\n\n // Collect all package names from both stores\n const allPackageNames = new Set<string>();\n for (const name of store.packages.keys()) allPackageNames.add(name);\n if (asyncStore) {\n for (const name of asyncStore.packages.keys()) allPackageNames.add(name);\n }\n\n const packages: PackageEntry[] = [];\n\n // 2. Process each package\n for (const packageName of allPackageNames) {\n const fileMap = store.packages.get(packageName);\n\n // Sum total CPU time for this package\n let packageTimeUs = 0;\n if (fileMap) {\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n packageTimeUs += us;\n }\n }\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 // Async totals for this package\n let packageAsyncTimeUs = 0;\n let packageAsyncOpCount = 0;\n const asyncFileMap = asyncStore?.packages.get(packageName);\n const asyncCountFileMap = asyncStore?.sampleCountsByPackage.get(packageName);\n if (asyncFileMap) {\n for (const funcMap of asyncFileMap.values()) {\n for (const us of funcMap.values()) {\n packageAsyncTimeUs += us;\n }\n }\n }\n if (asyncCountFileMap) {\n for (const countFuncMap of asyncCountFileMap.values()) {\n for (const count of countFuncMap.values()) {\n packageAsyncOpCount += count;\n }\n }\n }\n\n // 3. Collect all file names from both stores for this package\n const allFileNames = new Set<string>();\n if (fileMap) {\n for (const name of fileMap.keys()) allFileNames.add(name);\n }\n if (asyncFileMap) {\n for (const name of asyncFileMap.keys()) allFileNames.add(name);\n }\n\n const files: FileEntry[] = [];\n\n for (const fileName of allFileNames) {\n const funcMap = fileMap?.get(fileName);\n\n // Sum CPU time for this file\n let fileTimeUs = 0;\n if (funcMap) {\n for (const us of funcMap.values()) {\n fileTimeUs += us;\n }\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 // Async totals for this file\n let fileAsyncTimeUs = 0;\n let fileAsyncOpCount = 0;\n const asyncFuncMap = asyncFileMap?.get(fileName);\n const asyncCountFuncMap = asyncCountFileMap?.get(fileName);\n if (asyncFuncMap) {\n for (const us of asyncFuncMap.values()) {\n fileAsyncTimeUs += us;\n }\n }\n if (asyncCountFuncMap) {\n for (const count of asyncCountFuncMap.values()) {\n fileAsyncOpCount += count;\n }\n }\n\n // 4. Collect all function names from both stores for this file\n const allFuncNames = new Set<string>();\n if (funcMap) {\n for (const name of funcMap.keys()) allFuncNames.add(name);\n }\n if (asyncFuncMap) {\n for (const name of asyncFuncMap.keys()) allFuncNames.add(name);\n }\n\n const functions: FunctionEntry[] = [];\n\n for (const funcName of allFuncNames) {\n const funcTimeUs = funcMap?.get(funcName) ?? 0;\n const funcSampleCount = countFuncMap?.get(funcName) ?? 0;\n const funcAsyncTimeUs = asyncFuncMap?.get(funcName) ?? 0;\n const funcAsyncOpCount = asyncCountFuncMap?.get(funcName) ?? 0;\n\n const entry: FunctionEntry = {\n name: funcName,\n timeUs: funcTimeUs,\n pct: totalTimeUs > 0 ? (funcTimeUs / totalTimeUs) * 100 : 0,\n sampleCount: funcSampleCount,\n };\n\n if (totalAsyncTimeUs > 0) {\n entry.asyncTimeUs = funcAsyncTimeUs;\n entry.asyncPct = (funcAsyncTimeUs / totalAsyncTimeUs) * 100;\n entry.asyncOpCount = funcAsyncOpCount;\n }\n\n functions.push(entry);\n }\n\n // Sort functions by timeUs descending\n functions.sort((a, b) => b.timeUs - a.timeUs);\n\n const fileEntry: FileEntry = {\n name: fileName,\n timeUs: fileTimeUs,\n pct: totalTimeUs > 0 ? (fileTimeUs / totalTimeUs) * 100 : 0,\n sampleCount: fileSampleCount,\n functions,\n otherCount: 0,\n };\n\n if (totalAsyncTimeUs > 0) {\n fileEntry.asyncTimeUs = fileAsyncTimeUs;\n fileEntry.asyncPct = (fileAsyncTimeUs / totalAsyncTimeUs) * 100;\n fileEntry.asyncOpCount = fileAsyncOpCount;\n }\n\n files.push(fileEntry);\n }\n\n // Sort files by timeUs descending\n files.sort((a, b) => b.timeUs - a.timeUs);\n\n const pkgEntry: PackageEntry = {\n name: packageName,\n timeUs: packageTimeUs,\n pct: totalTimeUs > 0 ? (packageTimeUs / totalTimeUs) * 100 : 0,\n isFirstParty: packageName === projectName,\n sampleCount: packageSampleCount,\n files,\n otherCount: 0,\n };\n\n if (totalAsyncTimeUs > 0) {\n pkgEntry.asyncTimeUs = packageAsyncTimeUs;\n pkgEntry.asyncPct = (packageAsyncTimeUs / totalAsyncTimeUs) * 100;\n pkgEntry.asyncOpCount = packageAsyncOpCount;\n }\n\n packages.push(pkgEntry);\n }\n\n // Sort packages by timeUs descending\n packages.sort((a, b) => b.timeUs - a.timeUs);\n\n const result: ReportData = {\n timestamp: new Date().toLocaleString(),\n totalTimeUs,\n packages,\n otherCount: 0,\n projectName,\n };\n\n if (totalAsyncTimeUs > 0) {\n result.totalAsyncTimeUs = totalAsyncTimeUs;\n }\n\n return result;\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 { readFileSync } from \"node:fs\";\nimport type { Profiler } from \"node:inspector\";\nimport { Session } from \"node:inspector\";\nimport { join } from \"node:path\";\nimport { AsyncTracker } from \"./async-tracker.js\";\nimport { parseFrame } from \"./frame-parser.js\";\nimport { PackageResolver } from \"./package-resolver.js\";\nimport { PkgProfile } from \"./pkg-profile.js\";\nimport { aggregate } from \"./reporter/aggregate.js\";\nimport { SampleStore } from \"./sample-store.js\";\nimport type {\n ProfileCallbackOptions,\n RawCallFrame,\n StartOptions,\n} from \"./types.js\";\n\n// Module-level state -- lazy initialization\nlet session: Session | null = null;\nlet profiling = false;\nconst store = new SampleStore();\nconst asyncStore = new SampleStore();\nconst resolver = new PackageResolver(process.cwd());\nlet asyncTracker: AsyncTracker | null = null;\n\n/**\n * Promisify session.post for the normal async API path.\n */\nfunction postAsync(method: string, params?: object): Promise<any> {\n return new Promise<any>((resolve, reject) => {\n const cb: any = (err: Error | null, result?: any) => {\n if (err) reject(err);\n else resolve(result);\n };\n if (params !== undefined) {\n session!.post(method, params, cb);\n } else {\n session!.post(method, cb);\n }\n });\n}\n\n/**\n * Synchronous session.post — works because the V8 inspector executes\n * callbacks synchronously for in-process sessions.\n */\nfunction postSync(method: string): any {\n let result: any;\n let error: Error | null = null;\n const cb: any = (err: Error | null, params?: any) => {\n error = err;\n result = params;\n };\n session!.post(method, cb);\n if (error) throw error;\n return result;\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\nfunction buildEmptyProfile(): PkgProfile {\n const projectName = readProjectName(process.cwd());\n return new PkgProfile({\n timestamp: new Date().toLocaleString(),\n totalTimeUs: 0,\n packages: [],\n otherCount: 0,\n projectName,\n });\n}\n\n/**\n * Shared logic for stopping the profiler and building a PkgProfile.\n * Synchronous — safe to call from process `exit` handlers.\n */\nfunction stopSync(): PkgProfile {\n if (!profiling || !session) {\n return buildEmptyProfile();\n }\n\n const { profile } = postSync(\"Profiler.stop\") as Profiler.StopReturnType;\n postSync(\"Profiler.disable\");\n profiling = false;\n\n if (asyncTracker) {\n asyncTracker.disable();\n asyncTracker = null;\n }\n\n processProfile(profile);\n\n const projectName = readProjectName(process.cwd());\n const data = aggregate(\n store,\n projectName,\n asyncStore.packages.size > 0 ? asyncStore : undefined,\n );\n store.clear();\n asyncStore.clear();\n\n return new PkgProfile(data);\n}\n\n/**\n * Start the V8 CPU profiler. If already profiling, this is a safe no-op.\n *\n * @param options - Optional configuration.\n * @param options.interval - Sampling interval in microseconds passed to V8 (defaults to 1000µs). Lower values = higher fidelity but more overhead.\n * @returns Resolves when the profiler is successfully started\n */\nexport async function start(options?: StartOptions): Promise<void> {\n if (profiling) return;\n\n if (session === null) {\n session = new Session();\n session.connect();\n }\n\n await postAsync(\"Profiler.enable\");\n\n if (options?.interval !== undefined) {\n await postAsync(\"Profiler.setSamplingInterval\", {\n interval: options.interval,\n });\n }\n\n await postAsync(\"Profiler.start\");\n profiling = true;\n\n if (options?.trackAsync) {\n asyncTracker = new AsyncTracker(resolver, asyncStore);\n asyncTracker.enable();\n }\n}\n\n/**\n * Stop the profiler, process collected samples, and return a PkgProfile\n * containing the aggregated data. Resets the store afterward.\n *\n * @returns A PkgProfile with the profiling results, or a PkgProfile with empty data if no samples were collected.\n */\nexport async function stop(): Promise<PkgProfile> {\n return stopSync();\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 postSync(\"Profiler.stop\");\n postSync(\"Profiler.disable\");\n profiling = false;\n }\n store.clear();\n if (asyncTracker) {\n asyncTracker.disable();\n asyncTracker = null;\n }\n asyncStore.clear();\n}\n\n/**\n * High-level convenience for common profiling patterns.\n *\n * Overload 1: Profile a block of code — runs `fn`, stops the profiler, returns PkgProfile.\n * Overload 2: Long-running mode — starts profiler, registers exit handlers, calls `onExit` on shutdown.\n */\nexport async function profile(\n fn: () => void | Promise<void>,\n): Promise<PkgProfile>;\nexport async function profile(options: ProfileCallbackOptions): Promise<void>;\nexport async function profile(\n fnOrOptions: (() => void | Promise<void>) | ProfileCallbackOptions,\n): Promise<PkgProfile | void> {\n if (typeof fnOrOptions === \"function\") {\n await start();\n try {\n await fnOrOptions();\n } finally {\n return stop();\n }\n }\n\n // Long-running / onExit mode\n const { onExit, ...startOpts } = fnOrOptions;\n await start(startOpts);\n\n let handled = false;\n\n const handler = (signal?: NodeJS.Signals) => {\n if (handled) return;\n handled = true;\n\n process.removeListener(\"SIGINT\", onSignal);\n process.removeListener(\"SIGTERM\", onSignal);\n process.removeListener(\"exit\", onProcessExit);\n\n const result = stopSync();\n onExit(result);\n\n if (signal) {\n process.kill(process.pid, signal);\n }\n };\n\n const onSignal = (signal: NodeJS.Signals) => {\n handler(signal);\n };\n const onProcessExit = () => {\n handler();\n };\n\n process.once(\"SIGINT\", onSignal);\n process.once(\"SIGTERM\", onSignal);\n process.once(\"exit\", onProcessExit);\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(\n \"node (built-in)\",\n relativePath,\n parsed.functionId,\n deltaUs,\n );\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/async-tracker.ts","../src/frame-parser.ts","../src/package-resolver.ts","../src/reporter/format.ts","../src/reporter/html.ts","../src/pkg-profile.ts","../src/reporter/aggregate.ts","../src/sample-store.ts","../src/sampler.ts"],"sourcesContent":["/**\n * Opt-in async I/O wait time tracker using node:async_hooks.\n *\n * Tracks the time between async resource init (when the I/O op is started)\n * and the first before callback (when the callback fires), attributing\n * that wait time to the package/file/function that initiated the operation.\n *\n * Intervals are buffered and merged at disable() time so that overlapping\n * concurrent I/O is not double-counted.\n */\n\nimport { createHook } from 'node:async_hooks';\nimport type { AsyncHook } from 'node:async_hooks';\nimport type { PackageResolver } from './package-resolver.js';\nimport type { SampleStore } from './sample-store.js';\n\n/** Async resource types worth tracking — I/O and timers, not promises. */\nconst TRACKED_TYPES = new Set([\n 'TCPCONNECTWRAP',\n 'TCPWRAP',\n 'PIPEWRAP',\n 'PIPECONNECTWRAP',\n 'TLSWRAP',\n 'FSREQCALLBACK',\n 'FSREQPROMISE',\n 'GETADDRINFOREQWRAP',\n 'GETNAMEINFOREQWRAP',\n 'HTTPCLIENTREQUEST',\n 'HTTPINCOMINGMESSAGE',\n 'SHUTDOWNWRAP',\n 'WRITEWRAP',\n 'ZLIB',\n 'Timeout',\n]);\n\ninterface PendingOp {\n startHrtime: [number, number];\n pkg: string;\n file: string;\n fn: string;\n}\n\nexport interface Interval {\n startUs: number;\n endUs: number;\n}\n\n/**\n * Merge overlapping or adjacent intervals. Returns a new sorted array\n * of non-overlapping intervals.\n */\nexport function mergeIntervals(intervals: Interval[]): Interval[] {\n if (intervals.length <= 1) return intervals.slice();\n\n const sorted = intervals.slice().sort((a, b) => a.startUs - b.startUs);\n const merged: Interval[] = [{ ...sorted[0]! }];\n\n for (let i = 1; i < sorted.length; i++) {\n const current = sorted[i]!;\n const last = merged[merged.length - 1]!;\n\n if (current.startUs <= last.endUs) {\n // Overlapping or adjacent — extend\n if (current.endUs > last.endUs) {\n last.endUs = current.endUs;\n }\n } else {\n merged.push({ ...current });\n }\n }\n\n return merged;\n}\n\n/**\n * Sum the durations of a list of (presumably non-overlapping) intervals.\n */\nfunction sumIntervals(intervals: Interval[]): number {\n let total = 0;\n for (const iv of intervals) {\n total += iv.endUs - iv.startUs;\n }\n return total;\n}\n\n/**\n * Convert an hrtime tuple to absolute microseconds.\n */\nfunction hrtimeToUs(hr: [number, number]): number {\n return hr[0] * 1_000_000 + Math.round(hr[1] / 1000);\n}\n\n/**\n * Parse a single line from an Error().stack trace into file path and function id.\n * Returns null for lines that don't match V8's stack frame format or are node internals.\n *\n * Handles these V8 formats:\n * \" at functionName (/absolute/path:line:col)\"\n * \" at /absolute/path:line:col\"\n * \" at Object.functionName (/absolute/path:line:col)\"\n */\nexport function parseStackLine(line: string): { filePath: string; functionId: string } | null {\n // Match \" at [funcName] (filePath:line:col)\" or \" at filePath:line:col\"\n const match = line.match(/^\\s+at\\s+(?:(.+?)\\s+\\()?(.+?):(\\d+):\\d+\\)?$/);\n if (!match) return null;\n\n const rawFn = match[1] ?? '';\n const filePath = match[2]!;\n const lineNum = match[3]!;\n\n // Skip node internals (node:xxx, <anonymous>, etc)\n if (filePath.startsWith('node:') || filePath.startsWith('<')) return null;\n\n // Use last segment of function name (strip \"Object.\" etc)\n const fnParts = rawFn.split('.');\n const fnName = fnParts[fnParts.length - 1] || '<anonymous>';\n const functionId = `${fnName}:${lineNum}`;\n\n return { filePath, functionId };\n}\n\nexport class AsyncTracker {\n private readonly resolver: PackageResolver;\n private readonly thresholdUs: number;\n private hook: AsyncHook | null = null;\n private pending = new Map<number, PendingOp>();\n\n /** Buffered intervals keyed by \"pkg\\0file\\0fn\" */\n private keyedIntervals = new Map<string, Interval[]>();\n /** Flat list of all intervals for global merging */\n private globalIntervals: Interval[] = [];\n /** Origin time in absolute microseconds, set when enable() is called */\n private originUs = 0;\n\n /** Merged global total set after flush() */\n private _mergedTotalUs = 0;\n\n /**\n * @param resolver - PackageResolver for mapping file paths to packages\n * @param store - SampleStore to record async wait times into (used at flush time)\n * @param thresholdUs - Minimum wait duration in microseconds to record (default 1000 = 1ms)\n */\n constructor(resolver: PackageResolver, private readonly store: SampleStore, thresholdUs: number = 1000) {\n this.resolver = resolver;\n this.thresholdUs = thresholdUs;\n }\n\n /** Merged global async total in microseconds, available after disable(). */\n get mergedTotalUs(): number {\n return this._mergedTotalUs;\n }\n\n enable(): void {\n if (this.hook) return;\n\n this.originUs = hrtimeToUs(process.hrtime());\n\n this.hook = createHook({\n init: (asyncId: number, type: string) => {\n if (!TRACKED_TYPES.has(type)) return;\n\n // Capture stack trace with limited depth\n const holder: { stack?: string } = {};\n const origLimit = Error.stackTraceLimit;\n Error.stackTraceLimit = 8;\n Error.captureStackTrace(holder);\n Error.stackTraceLimit = origLimit;\n\n const stack = holder.stack;\n if (!stack) return;\n\n // Find the first user-code frame (skip async_hooks internals)\n const lines = stack.split('\\n');\n let parsed: { filePath: string; functionId: string } | null = null;\n for (let i = 1; i < lines.length; i++) {\n const result = parseStackLine(lines[i]!);\n if (result) {\n // Skip frames inside this module\n if (result.filePath.includes('async-tracker')) continue;\n parsed = result;\n break;\n }\n }\n\n if (!parsed) return;\n\n // Resolve to package\n const { packageName, relativePath } = this.resolver.resolve(parsed.filePath);\n\n this.pending.set(asyncId, {\n startHrtime: process.hrtime(),\n pkg: packageName,\n file: relativePath,\n fn: parsed.functionId,\n });\n },\n\n before: (asyncId: number) => {\n const op = this.pending.get(asyncId);\n if (!op) return;\n\n const endHr = process.hrtime();\n const startUs = hrtimeToUs(op.startHrtime);\n const endUs = hrtimeToUs(endHr);\n const durationUs = endUs - startUs;\n\n if (durationUs >= this.thresholdUs) {\n const interval: Interval = { startUs, endUs };\n const key = `${op.pkg}\\0${op.file}\\0${op.fn}`;\n\n let arr = this.keyedIntervals.get(key);\n if (!arr) {\n arr = [];\n this.keyedIntervals.set(key, arr);\n }\n arr.push(interval);\n\n this.globalIntervals.push(interval);\n }\n\n this.pending.delete(asyncId);\n },\n\n destroy: (asyncId: number) => {\n // Clean up ops that never got a before callback (aborted)\n this.pending.delete(asyncId);\n },\n });\n\n this.hook.enable();\n }\n\n disable(): void {\n if (!this.hook) return;\n\n this.hook.disable();\n\n // Resolve any pending ops using current time\n const nowHr = process.hrtime();\n const nowUs = hrtimeToUs(nowHr);\n for (const [, op] of this.pending) {\n const startUs = hrtimeToUs(op.startHrtime);\n const durationUs = nowUs - startUs;\n\n if (durationUs >= this.thresholdUs) {\n const interval: Interval = { startUs, endUs: nowUs };\n const key = `${op.pkg}\\0${op.file}\\0${op.fn}`;\n\n let arr = this.keyedIntervals.get(key);\n if (!arr) {\n arr = [];\n this.keyedIntervals.set(key, arr);\n }\n arr.push(interval);\n\n this.globalIntervals.push(interval);\n }\n }\n\n this.pending.clear();\n this.hook = null;\n\n this.flush();\n }\n\n /**\n * Merge buffered intervals and record to the store.\n * Sets mergedTotalUs to the global merged duration.\n */\n private flush(): void {\n // Per-key: merge overlapping intervals, sum durations, record to store\n for (const [key, intervals] of this.keyedIntervals) {\n const merged = mergeIntervals(intervals);\n const totalUs = sumIntervals(merged);\n if (totalUs > 0) {\n const parts = key.split('\\0');\n this.store.record(parts[0]!, parts[1]!, parts[2]!, totalUs);\n }\n }\n\n // Global: merge all intervals to compute real elapsed async wait\n const globalMerged = mergeIntervals(this.globalIntervals);\n this._mergedTotalUs = sumIntervals(globalMerged);\n\n // Clean up buffers\n this.keyedIntervals.clear();\n this.globalIntervals = [];\n }\n}\n","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 *\n * @param frame - Raw call frame from the V8 CPU profiler.\n * @returns A classified frame: `'user'` with file path and function id, or a non-user kind.\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 * 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 *\n * @param us - Time value in microseconds.\n * @returns Human-readable time string.\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 *\n * @param us - Time value in microseconds.\n * @param totalUs - Total time in microseconds (denominator).\n * @returns Percentage string like `\"12.3%\"`.\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 *\n * @param str - Raw string to escape.\n * @returns HTML-safe string.\n */\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","/**\n * HTML renderer for the profiling report.\n *\n * Generates a self-contained HTML file (inline CSS/JS, no external dependencies)\n * with a summary table, expandable Package > File > Function tree, and an\n * interactive threshold slider that filters data client-side.\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 --bar-fill-async: #f5943b;\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 /* Threshold slider */\n .threshold-control {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-bottom: 1rem;\n font-size: 0.85rem;\n }\n\n .threshold-control label {\n font-weight: 600;\n color: var(--muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n font-size: 0.8rem;\n }\n\n .threshold-control input[type=\"range\"] {\n flex: 1;\n max-width: 240px;\n height: 8px;\n appearance: none;\n -webkit-appearance: none;\n background: var(--bar-track);\n border-radius: 4px;\n outline: none;\n }\n\n .threshold-control input[type=\"range\"]::-webkit-slider-thumb {\n appearance: none;\n -webkit-appearance: none;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--bar-fill);\n cursor: pointer;\n }\n\n .threshold-control input[type=\"range\"]::-moz-range-thumb {\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--bar-fill);\n cursor: pointer;\n border: none;\n }\n\n .threshold-control span {\n font-family: var(--font-mono);\n font-size: 0.85rem;\n min-width: 3.5em;\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 td.async-col { color: var(--bar-fill-async); }\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 .tree-async {\n font-family: var(--font-mono);\n font-size: 0.8rem;\n color: var(--bar-fill-async);\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 /* Sort control */\n .sort-control {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n margin-left: 1.5rem;\n font-size: 0.85rem;\n }\n\n .sort-control label {\n font-weight: 600;\n color: var(--muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n font-size: 0.8rem;\n }\n\n .sort-toggle {\n display: inline-flex;\n border: 1px solid var(--border);\n border-radius: 4px;\n overflow: hidden;\n }\n\n .sort-toggle button {\n font-family: var(--font-sans);\n font-size: 0.8rem;\n padding: 0.25rem 0.6rem;\n border: none;\n background: #fff;\n color: var(--muted);\n cursor: pointer;\n transition: background 0.15s, color 0.15s;\n }\n\n .sort-toggle button + button {\n border-left: 1px solid var(--border);\n }\n\n .sort-toggle button.active {\n background: var(--bar-fill);\n color: #fff;\n }\n\n .sort-toggle button.active-async {\n background: var(--bar-fill-async);\n color: #fff;\n }\n\n @media (max-width: 600px) {\n body { padding: 1rem; }\n .bar-cell { width: 25%; }\n .sort-control { margin-left: 0; margin-top: 0.5rem; }\n }\n `;\n}\n\nfunction generateJs(): string {\n return `\n(function() {\n var DATA = window.__REPORT_DATA__;\n if (!DATA) return;\n var HAS_ASYNC = !!(DATA.totalAsyncTimeUs && DATA.totalAsyncTimeUs > 0);\n\n function formatTime(us) {\n if (us === 0) return '0ms';\n var ms = us / 1000;\n if (ms >= 1000) return (ms / 1000).toFixed(2) + 's';\n var rounded = Math.round(ms);\n return (rounded < 1 ? 1 : rounded) + 'ms';\n }\n\n function formatPct(us, totalUs) {\n if (totalUs === 0) return '0.0%';\n return ((us / totalUs) * 100).toFixed(1) + '%';\n }\n\n function escapeHtml(str) {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n }\n\n var sortBy = 'cpu';\n\n function metricTime(entry) {\n return sortBy === 'async' ? (entry.asyncTimeUs || 0) : entry.timeUs;\n }\n\n function sortDesc(arr) {\n return arr.slice().sort(function(a, b) { return metricTime(b) - metricTime(a); });\n }\n\n function applyThreshold(data, pct) {\n var totalBase = sortBy === 'async' ? (data.totalAsyncTimeUs || 0) : data.totalTimeUs;\n var threshold = totalBase * (pct / 100);\n var filtered = [];\n var otherCount = 0;\n\n var pkgs = sortDesc(data.packages);\n\n for (var i = 0; i < pkgs.length; i++) {\n var pkg = pkgs[i];\n if (metricTime(pkg) < threshold) {\n otherCount++;\n continue;\n }\n\n var files = [];\n var fileOtherCount = 0;\n\n var sortedFiles = sortDesc(pkg.files);\n\n for (var j = 0; j < sortedFiles.length; j++) {\n var file = sortedFiles[j];\n if (metricTime(file) < threshold) {\n fileOtherCount++;\n continue;\n }\n\n var functions = [];\n var funcOtherCount = 0;\n\n var sortedFns = sortDesc(file.functions);\n\n for (var k = 0; k < sortedFns.length; k++) {\n var fn = sortedFns[k];\n if (metricTime(fn) < threshold) {\n funcOtherCount++;\n continue;\n }\n functions.push(fn);\n }\n\n files.push({\n name: file.name,\n timeUs: file.timeUs,\n pct: file.pct,\n sampleCount: file.sampleCount,\n asyncTimeUs: file.asyncTimeUs,\n asyncPct: file.asyncPct,\n asyncOpCount: file.asyncOpCount,\n functions: functions,\n otherCount: funcOtherCount\n });\n }\n\n filtered.push({\n name: pkg.name,\n timeUs: pkg.timeUs,\n pct: pkg.pct,\n isFirstParty: pkg.isFirstParty,\n sampleCount: pkg.sampleCount,\n asyncTimeUs: pkg.asyncTimeUs,\n asyncPct: pkg.asyncPct,\n asyncOpCount: pkg.asyncOpCount,\n files: files,\n otherCount: fileOtherCount\n });\n }\n\n return { packages: filtered, otherCount: otherCount };\n }\n\n function renderTable(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {\n var rows = '';\n var isAsync = sortBy === 'async';\n var barTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;\n for (var i = 0; i < packages.length; i++) {\n var pkg = packages[i];\n var cls = pkg.isFirstParty ? 'first-party' : 'dependency';\n var barVal = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;\n var pctVal = barTotal > 0 ? (barVal / barTotal) * 100 : 0;\n rows += '<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\"><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(barVal, barTotal)) + '</span>' +\n '</div></td>' +\n '<td class=\"numeric\">' + pkg.sampleCount + '</td>';\n if (HAS_ASYNC) {\n rows += '<td class=\"numeric async-col\">' + escapeHtml(formatTime(pkg.asyncTimeUs || 0)) + '</td>' +\n '<td class=\"numeric async-col\">' + (pkg.asyncOpCount || 0) + '</td>';\n }\n rows += '</tr>';\n }\n\n if (otherCount > 0) {\n rows += '<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 if (HAS_ASYNC) {\n rows += '<td class=\"numeric\"></td><td class=\"numeric\"></td>';\n }\n rows += '</tr>';\n }\n\n var headers = '<th>Package</th><th>CPU Time</th><th>% of Total</th><th>Samples</th>';\n if (HAS_ASYNC) {\n headers += '<th>Async I/O Wait</th><th>Async Ops</th>';\n }\n\n return '<table><thead><tr>' + headers + '</tr></thead><tbody>' + rows + '</tbody></table>';\n }\n\n function asyncStats(entry) {\n if (!HAS_ASYNC) return '';\n var at = entry.asyncTimeUs || 0;\n var ac = entry.asyncOpCount || 0;\n if (at === 0 && ac === 0) return '';\n return ' <span class=\"tree-async\">| ' + escapeHtml(formatTime(at)) + ' async · ' + ac + ' ops</span>';\n }\n\n function renderTree(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {\n var html = '<div class=\"tree\">';\n var isAsync = sortBy === 'async';\n var pctTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;\n\n for (var i = 0; i < packages.length; i++) {\n var pkg = packages[i];\n var fpCls = pkg.isFirstParty ? ' fp-pkg' : '';\n var pkgTime = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;\n html += '<details class=\"level-0' + fpCls + '\"><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(pkgTime)) + ' · ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' · ' + pkg.sampleCount + ' samples</span>';\n html += asyncStats(pkg);\n html += '</summary>';\n\n for (var j = 0; j < pkg.files.length; j++) {\n var file = pkg.files[j];\n var fileTime = isAsync ? (file.asyncTimeUs || 0) : file.timeUs;\n html += '<details class=\"level-1\"><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(fileTime)) + ' · ' + escapeHtml(formatPct(fileTime, pctTotal)) + ' · ' + file.sampleCount + ' samples</span>';\n html += asyncStats(file);\n html += '</summary>';\n\n for (var k = 0; k < file.functions.length; k++) {\n var fn = file.functions[k];\n var fnTime = isAsync ? (fn.asyncTimeUs || 0) : fn.timeUs;\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(fnTime)) + ' · ' + escapeHtml(formatPct(fnTime, pctTotal)) + ' · ' + fn.sampleCount + ' samples</span>';\n html += asyncStats(fn);\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 var currentThreshold = 5;\n\n function update(pct) {\n currentThreshold = pct;\n var result = applyThreshold(DATA, pct);\n var summaryEl = document.getElementById('summary-container');\n var treeEl = document.getElementById('tree-container');\n if (summaryEl) summaryEl.innerHTML = renderTable(result.packages, result.otherCount, DATA.totalTimeUs, DATA.totalAsyncTimeUs);\n if (treeEl) treeEl.innerHTML = renderTree(result.packages, result.otherCount, DATA.totalTimeUs, DATA.totalAsyncTimeUs);\n }\n\n function updateSortButtons() {\n var btns = document.querySelectorAll('.sort-toggle button');\n for (var i = 0; i < btns.length; i++) {\n var btn = btns[i];\n btn.className = '';\n if (btn.getAttribute('data-sort') === sortBy) {\n btn.className = sortBy === 'async' ? 'active-async' : 'active';\n }\n }\n }\n\n document.addEventListener('DOMContentLoaded', function() {\n update(5);\n var slider = document.getElementById('threshold-slider');\n var label = document.getElementById('threshold-value');\n if (slider) {\n slider.addEventListener('input', function() {\n var val = parseFloat(slider.value);\n if (label) label.textContent = val.toFixed(1) + '%';\n update(val);\n });\n }\n\n var sortBtns = document.querySelectorAll('.sort-toggle button');\n for (var i = 0; i < sortBtns.length; i++) {\n sortBtns[i].addEventListener('click', function() {\n sortBy = this.getAttribute('data-sort') || 'cpu';\n updateSortButtons();\n update(currentThreshold);\n });\n }\n });\n})();\n`;\n}\n\nfunction renderSummaryTable(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n hasAsync: boolean,\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>${hasAsync ? `\n <td class=\"numeric async-col\">${escapeHtml(formatTime(pkg.asyncTimeUs ?? 0))}</td>\n <td class=\"numeric async-col\">${pkg.asyncOpCount ?? 0}</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>${hasAsync ? `\n <td class=\"numeric\"></td>\n <td class=\"numeric\"></td>` : ''}\n </tr>`;\n }\n\n return `\n <table>\n <thead>\n <tr>\n <th>Package</th>\n <th>CPU Time</th>\n <th>% of Total</th>\n <th>Samples</th>${hasAsync ? `\n <th>Async I/O Wait</th>\n <th>Async Ops</th>` : ''}\n </tr>\n </thead>\n <tbody>${rows}\n </tbody>\n </table>`;\n}\n\nfunction formatAsyncStats(entry: { asyncTimeUs?: number; asyncOpCount?: number }): string {\n const at = entry.asyncTimeUs ?? 0;\n const ac = entry.asyncOpCount ?? 0;\n if (at === 0 && ac === 0) return '';\n return ` <span class=\"tree-async\">| ${escapeHtml(formatTime(at))} async · ${ac} ops</span>`;\n}\n\nfunction renderTree(\n packages: PackageEntry[],\n otherCount: number,\n totalTimeUs: number,\n hasAsync: boolean,\n): string {\n let html = '<div class=\"tree\">';\n\n for (const pkg of packages) {\n const fpCls = pkg.isFirstParty ? ' fp-pkg' : '';\n html += `<details class=\"level-0${fpCls}\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label pkg\">pkg</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(pkg.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;\n if (hasAsync) html += formatAsyncStats(pkg);\n html += `</summary>`;\n\n for (const file of pkg.files) {\n html += `<details class=\"level-1\">`;\n html += `<summary>`;\n html += `<span class=\"tree-label file\">file</span>`;\n html += `<span class=\"tree-name\">${escapeHtml(file.name)}</span>`;\n html += `<span class=\"tree-stats\">${escapeHtml(formatTime(file.timeUs))} · ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} · ${file.sampleCount} samples</span>`;\n if (hasAsync) html += formatAsyncStats(file);\n html += `</summary>`;\n\n for (const fn of file.functions) {\n html += `<div class=\"level-2\">`;\n html += `<span class=\"tree-label fn\">fn</span> `;\n html += `<span class=\"tree-name\">${escapeHtml(fn.name)}</span>`;\n html += ` <span class=\"tree-stats\">${escapeHtml(formatTime(fn.timeUs))} · ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} · ${fn.sampleCount} samples</span>`;\n if (hasAsync) html += formatAsyncStats(fn);\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 *\n * @param data - Aggregated report data (packages, timing, project name).\n * @returns A full HTML document string with inline CSS/JS and no external dependencies.\n */\nexport function renderHtml(data: ReportData): string {\n const hasAsync = !!(data.totalAsyncTimeUs && data.totalAsyncTimeUs > 0);\n const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs, hasAsync);\n const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs, hasAsync);\n const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));\n\n const titleName = escapeHtml(data.projectName);\n\n const wallFormatted = data.wallTimeUs ? escapeHtml(formatTime(data.wallTimeUs)) : null;\n let metaLine = `Generated ${escapeHtml(data.timestamp)}`;\n if (wallFormatted) {\n metaLine += ` · Wall time: ${wallFormatted}`;\n }\n metaLine += ` · CPU time: ${totalFormatted}`;\n if (hasAsync) {\n metaLine += ` · Async I/O wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs!))}`;\n }\n\n // Sanitize JSON for safe embedding in <script> — replace < to prevent </script> injection\n const safeJson = JSON.stringify(data).replace(/</g, '\\\\u003c');\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\">${metaLine}</div>\n\n <h2>Summary</h2>\n <div class=\"threshold-control\">\n <label>Threshold</label>\n <input type=\"range\" id=\"threshold-slider\" min=\"0\" max=\"20\" step=\"0.5\" value=\"5\">\n <span id=\"threshold-value\">5.0%</span>${hasAsync ? `\n <span class=\"sort-control\">\n <label>Sort by</label>\n <span class=\"sort-toggle\">\n <button data-sort=\"cpu\" class=\"active\">CPU Time</button>\n <button data-sort=\"async\">Async I/O Wait</button>\n </span>\n </span>` : ''}\n </div>\n <div id=\"summary-container\">${summaryTable}</div>\n\n <h2>Details</h2>\n <div id=\"tree-container\">${tree}</div>\n\n <script>var __REPORT_DATA__ = ${safeJson};</script>\n <script>${generateJs()}</script>\n</body>\n</html>`;\n}\n","/**\n * Immutable profiling result returned by `stop()` and `profile()`.\n *\n * Contains aggregated per-package timing data and a convenience method\n * to write a self-contained HTML report to disk.\n */\n\nimport { writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport type { PackageEntry, ReportData } from './types.js';\nimport { renderHtml } from './reporter/html.js';\n\nfunction generateFilename(timestamp: string): 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\nexport class PkgProfile {\n /** When the profile was captured */\n readonly timestamp: string;\n /** Total sampled wall time in microseconds */\n readonly totalTimeUs: number;\n /** Package breakdown sorted by time descending (all packages, no threshold applied) */\n readonly packages: PackageEntry[];\n /** Always 0 — threshold filtering is now applied client-side in the HTML report */\n readonly otherCount: number;\n /** Project name (from package.json) */\n readonly projectName: string;\n /** Total async wait time in microseconds (undefined when async tracking not enabled) */\n readonly totalAsyncTimeUs?: number;\n /** Elapsed wall time in microseconds from start() to stop() */\n readonly wallTimeUs?: number;\n\n /** @internal */\n constructor(data: ReportData) {\n this.timestamp = data.timestamp;\n this.totalTimeUs = data.totalTimeUs;\n this.packages = data.packages;\n this.otherCount = data.otherCount;\n this.projectName = data.projectName;\n this.totalAsyncTimeUs = data.totalAsyncTimeUs;\n this.wallTimeUs = data.wallTimeUs;\n }\n\n /**\n * Write a self-contained HTML report to disk.\n *\n * @param path - Output file path. Defaults to `./where-you-at-{timestamp}.html` in cwd.\n * @returns Absolute path to the written file.\n */\n writeHtml(path?: string): string {\n const data: ReportData = {\n timestamp: this.timestamp,\n totalTimeUs: this.totalTimeUs,\n packages: this.packages,\n otherCount: this.otherCount,\n projectName: this.projectName,\n totalAsyncTimeUs: this.totalAsyncTimeUs,\n wallTimeUs: this.wallTimeUs,\n };\n const html = renderHtml(data);\n\n let filepath: string;\n if (path) {\n filepath = resolve(path);\n } else {\n const filename = generateFilename(this.timestamp);\n filepath = join(process.cwd(), filename);\n }\n\n writeFileSync(filepath, html, 'utf-8');\n return filepath;\n }\n}\n","/**\n * Transform raw SampleStore data into a sorted ReportData structure.\n *\n * Pure function: reads nested Maps from SampleStore, computes percentages,\n * and sorts by timeUs descending at each level. No threshold filtering —\n * all entries are included so the HTML report can apply thresholds client-side.\n */\n\nimport type { SampleStore } from '../sample-store.js';\nimport type {\n ReportData,\n PackageEntry,\n FileEntry,\n FunctionEntry,\n} from '../types.js';\n\n/**\n * Sum all microseconds in a SampleStore.\n */\nfunction sumStore(store: SampleStore): number {\n let total = 0;\n for (const fileMap of store.packages.values()) {\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n total += us;\n }\n }\n }\n return total;\n}\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 * @param asyncStore - Optional SampleStore with async wait time data\n * @returns ReportData with all packages sorted desc by time, no threshold applied\n */\nexport function aggregate(store: SampleStore, projectName: string, asyncStore?: SampleStore, globalAsyncTimeUs?: number, wallTimeUs?: number): ReportData {\n // 1. Calculate total user-attributed time\n const totalTimeUs = sumStore(store);\n // Per-entry percentages use the raw sum so they add up to 100%\n const totalAsyncTimeUs = asyncStore ? sumStore(asyncStore) : 0;\n // Header total uses the merged (de-duplicated) global value when available\n const headerAsyncTimeUs = globalAsyncTimeUs ?? totalAsyncTimeUs;\n\n if (totalTimeUs === 0 && totalAsyncTimeUs === 0) {\n return {\n timestamp: new Date().toLocaleString(),\n totalTimeUs: 0,\n packages: [],\n otherCount: 0,\n projectName,\n };\n }\n\n // Collect all package names from both stores\n const allPackageNames = new Set<string>();\n for (const name of store.packages.keys()) allPackageNames.add(name);\n if (asyncStore) {\n for (const name of asyncStore.packages.keys()) allPackageNames.add(name);\n }\n\n const packages: PackageEntry[] = [];\n\n // 2. Process each package\n for (const packageName of allPackageNames) {\n const fileMap = store.packages.get(packageName);\n\n // Sum total CPU time for this package\n let packageTimeUs = 0;\n if (fileMap) {\n for (const funcMap of fileMap.values()) {\n for (const us of funcMap.values()) {\n packageTimeUs += us;\n }\n }\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 // Async totals for this package\n let packageAsyncTimeUs = 0;\n let packageAsyncOpCount = 0;\n const asyncFileMap = asyncStore?.packages.get(packageName);\n const asyncCountFileMap = asyncStore?.sampleCountsByPackage.get(packageName);\n if (asyncFileMap) {\n for (const funcMap of asyncFileMap.values()) {\n for (const us of funcMap.values()) {\n packageAsyncTimeUs += us;\n }\n }\n }\n if (asyncCountFileMap) {\n for (const countFuncMap of asyncCountFileMap.values()) {\n for (const count of countFuncMap.values()) {\n packageAsyncOpCount += count;\n }\n }\n }\n\n // 3. Collect all file names from both stores for this package\n const allFileNames = new Set<string>();\n if (fileMap) {\n for (const name of fileMap.keys()) allFileNames.add(name);\n }\n if (asyncFileMap) {\n for (const name of asyncFileMap.keys()) allFileNames.add(name);\n }\n\n const files: FileEntry[] = [];\n\n for (const fileName of allFileNames) {\n const funcMap = fileMap?.get(fileName);\n\n // Sum CPU time for this file\n let fileTimeUs = 0;\n if (funcMap) {\n for (const us of funcMap.values()) {\n fileTimeUs += us;\n }\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 // Async totals for this file\n let fileAsyncTimeUs = 0;\n let fileAsyncOpCount = 0;\n const asyncFuncMap = asyncFileMap?.get(fileName);\n const asyncCountFuncMap = asyncCountFileMap?.get(fileName);\n if (asyncFuncMap) {\n for (const us of asyncFuncMap.values()) {\n fileAsyncTimeUs += us;\n }\n }\n if (asyncCountFuncMap) {\n for (const count of asyncCountFuncMap.values()) {\n fileAsyncOpCount += count;\n }\n }\n\n // 4. Collect all function names from both stores for this file\n const allFuncNames = new Set<string>();\n if (funcMap) {\n for (const name of funcMap.keys()) allFuncNames.add(name);\n }\n if (asyncFuncMap) {\n for (const name of asyncFuncMap.keys()) allFuncNames.add(name);\n }\n\n const functions: FunctionEntry[] = [];\n\n for (const funcName of allFuncNames) {\n const funcTimeUs = funcMap?.get(funcName) ?? 0;\n const funcSampleCount = countFuncMap?.get(funcName) ?? 0;\n const funcAsyncTimeUs = asyncFuncMap?.get(funcName) ?? 0;\n const funcAsyncOpCount = asyncCountFuncMap?.get(funcName) ?? 0;\n\n const entry: FunctionEntry = {\n name: funcName,\n timeUs: funcTimeUs,\n pct: totalTimeUs > 0 ? (funcTimeUs / totalTimeUs) * 100 : 0,\n sampleCount: funcSampleCount,\n };\n\n if (totalAsyncTimeUs > 0) {\n entry.asyncTimeUs = funcAsyncTimeUs;\n entry.asyncPct = (funcAsyncTimeUs / totalAsyncTimeUs) * 100;\n entry.asyncOpCount = funcAsyncOpCount;\n }\n\n functions.push(entry);\n }\n\n // Sort functions by timeUs descending\n functions.sort((a, b) => b.timeUs - a.timeUs);\n\n const fileEntry: FileEntry = {\n name: fileName,\n timeUs: fileTimeUs,\n pct: totalTimeUs > 0 ? (fileTimeUs / totalTimeUs) * 100 : 0,\n sampleCount: fileSampleCount,\n functions,\n otherCount: 0,\n };\n\n if (totalAsyncTimeUs > 0) {\n fileEntry.asyncTimeUs = fileAsyncTimeUs;\n fileEntry.asyncPct = (fileAsyncTimeUs / totalAsyncTimeUs) * 100;\n fileEntry.asyncOpCount = fileAsyncOpCount;\n }\n\n files.push(fileEntry);\n }\n\n // Sort files by timeUs descending\n files.sort((a, b) => b.timeUs - a.timeUs);\n\n const pkgEntry: PackageEntry = {\n name: packageName,\n timeUs: packageTimeUs,\n pct: totalTimeUs > 0 ? (packageTimeUs / totalTimeUs) * 100 : 0,\n isFirstParty: packageName === projectName,\n sampleCount: packageSampleCount,\n files,\n otherCount: 0,\n };\n\n if (totalAsyncTimeUs > 0) {\n pkgEntry.asyncTimeUs = packageAsyncTimeUs;\n pkgEntry.asyncPct = (packageAsyncTimeUs / totalAsyncTimeUs) * 100;\n pkgEntry.asyncOpCount = packageAsyncOpCount;\n }\n\n packages.push(pkgEntry);\n }\n\n // Sort packages by timeUs descending\n packages.sort((a, b) => b.timeUs - a.timeUs);\n\n const result: ReportData = {\n timestamp: new Date().toLocaleString(),\n totalTimeUs,\n packages,\n otherCount: 0,\n projectName,\n };\n\n if (headerAsyncTimeUs > 0) {\n result.totalAsyncTimeUs = headerAsyncTimeUs;\n }\n\n if (wallTimeUs !== undefined) {\n result.wallTimeUs = wallTimeUs;\n }\n\n return result;\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 { readFileSync } from \"node:fs\";\nimport type { Profiler } from \"node:inspector\";\nimport { Session } from \"node:inspector\";\nimport { join } from \"node:path\";\nimport { AsyncTracker } from \"./async-tracker.js\";\nimport { parseFrame } from \"./frame-parser.js\";\nimport { PackageResolver } from \"./package-resolver.js\";\nimport { PkgProfile } from \"./pkg-profile.js\";\nimport { aggregate } from \"./reporter/aggregate.js\";\nimport { SampleStore } from \"./sample-store.js\";\nimport type {\n ProfileCallbackOptions,\n RawCallFrame,\n StartOptions,\n} from \"./types.js\";\n\n// Module-level state -- lazy initialization\nlet session: Session | null = null;\nlet profiling = false;\nlet startHrtime: [number, number] | null = null;\nconst store = new SampleStore();\nconst asyncStore = new SampleStore();\nconst resolver = new PackageResolver(process.cwd());\nlet asyncTracker: AsyncTracker | null = null;\n\n/**\n * Promisify session.post for the normal async API path.\n */\nfunction postAsync(method: string, params?: object): Promise<any> {\n return new Promise<any>((resolve, reject) => {\n const cb: any = (err: Error | null, result?: any) => {\n if (err) reject(err);\n else resolve(result);\n };\n if (params !== undefined) {\n session!.post(method, params, cb);\n } else {\n session!.post(method, cb);\n }\n });\n}\n\n/**\n * Synchronous session.post — works because the V8 inspector executes\n * callbacks synchronously for in-process sessions.\n */\nfunction postSync(method: string): any {\n let result: any;\n let error: Error | null = null;\n const cb: any = (err: Error | null, params?: any) => {\n error = err;\n result = params;\n };\n session!.post(method, cb);\n if (error) throw error;\n return result;\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\nfunction buildEmptyProfile(): PkgProfile {\n const projectName = readProjectName(process.cwd());\n return new PkgProfile({\n timestamp: new Date().toLocaleString(),\n totalTimeUs: 0,\n packages: [],\n otherCount: 0,\n projectName,\n });\n}\n\n/**\n * Shared logic for stopping the profiler and building a PkgProfile.\n * Synchronous — safe to call from process `exit` handlers.\n */\nfunction stopSync(): PkgProfile {\n if (!profiling || !session) {\n return buildEmptyProfile();\n }\n\n const elapsed = startHrtime ? process.hrtime(startHrtime) : null;\n const wallTimeUs = elapsed ? elapsed[0] * 1_000_000 + Math.round(elapsed[1] / 1000) : undefined;\n startHrtime = null;\n\n const { profile } = postSync(\"Profiler.stop\") as Profiler.StopReturnType;\n postSync(\"Profiler.disable\");\n profiling = false;\n\n let globalAsyncTimeUs: number | undefined;\n if (asyncTracker) {\n asyncTracker.disable();\n globalAsyncTimeUs = asyncTracker.mergedTotalUs;\n asyncTracker = null;\n }\n\n processProfile(profile);\n\n const projectName = readProjectName(process.cwd());\n const data = aggregate(\n store,\n projectName,\n asyncStore.packages.size > 0 ? asyncStore : undefined,\n globalAsyncTimeUs,\n wallTimeUs,\n );\n store.clear();\n asyncStore.clear();\n\n return new PkgProfile(data);\n}\n\n/**\n * Start the V8 CPU profiler. If already profiling, this is a safe no-op.\n *\n * @param options - Optional configuration.\n * @param options.interval - Sampling interval in microseconds passed to V8 (defaults to 1000µs). Lower values = higher fidelity but more overhead.\n * @returns Resolves when the profiler is successfully started\n */\nexport async function start(options?: StartOptions): Promise<void> {\n if (profiling) return;\n\n if (session === null) {\n session = new Session();\n session.connect();\n }\n\n await postAsync(\"Profiler.enable\");\n\n if (options?.interval !== undefined) {\n await postAsync(\"Profiler.setSamplingInterval\", {\n interval: options.interval,\n });\n }\n\n await postAsync(\"Profiler.start\");\n profiling = true;\n startHrtime = process.hrtime();\n\n if (options?.trackAsync) {\n asyncTracker = new AsyncTracker(resolver, asyncStore);\n asyncTracker.enable();\n }\n}\n\n/**\n * Stop the profiler, process collected samples, and return a PkgProfile\n * containing the aggregated data. Resets the store afterward.\n *\n * @returns A PkgProfile with the profiling results, or a PkgProfile with empty data if no samples were collected.\n */\nexport async function stop(): Promise<PkgProfile> {\n return stopSync();\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 postSync(\"Profiler.stop\");\n postSync(\"Profiler.disable\");\n profiling = false;\n }\n startHrtime = null;\n store.clear();\n if (asyncTracker) {\n asyncTracker.disable();\n asyncTracker = null;\n }\n asyncStore.clear();\n}\n\n/**\n * High-level convenience for common profiling patterns.\n *\n * Overload 1: Profile a block of code — runs `fn`, stops the profiler, returns PkgProfile.\n * Overload 2: Long-running mode — starts profiler, registers exit handlers, calls `onExit` on shutdown.\n */\nexport async function profile(\n fn: () => void | Promise<void>,\n): Promise<PkgProfile>;\nexport async function profile(options: ProfileCallbackOptions): Promise<void>;\nexport async function profile(\n fnOrOptions: (() => void | Promise<void>) | ProfileCallbackOptions,\n): Promise<PkgProfile | void> {\n if (typeof fnOrOptions === \"function\") {\n await start();\n try {\n await fnOrOptions();\n } finally {\n return stop();\n }\n }\n\n // Long-running / onExit mode\n const { onExit, ...startOpts } = fnOrOptions;\n await start(startOpts);\n\n let handled = false;\n\n const handler = (signal?: NodeJS.Signals) => {\n if (handled) return;\n handled = true;\n\n process.removeListener(\"SIGINT\", onSignal);\n process.removeListener(\"SIGTERM\", onSignal);\n process.removeListener(\"exit\", onProcessExit);\n\n const result = stopSync();\n onExit(result);\n\n if (signal) {\n process.kill(process.pid, signal);\n }\n };\n\n const onSignal = (signal: NodeJS.Signals) => {\n handler(signal);\n };\n const onProcessExit = () => {\n handler();\n };\n\n process.once(\"SIGINT\", onSignal);\n process.once(\"SIGTERM\", onSignal);\n process.once(\"exit\", onProcessExit);\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(\n \"node (built-in)\",\n relativePath,\n parsed.functionId,\n deltaUs,\n );\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
package/src/async-tracker.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Tracks the time between async resource init (when the I/O op is started)
|
|
5
5
|
* and the first before callback (when the callback fires), attributing
|
|
6
6
|
* that wait time to the package/file/function that initiated the operation.
|
|
7
|
+
*
|
|
8
|
+
* Intervals are buffered and merged at disable() time so that overlapping
|
|
9
|
+
* concurrent I/O is not double-counted.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { createHook } from 'node:async_hooks';
|
|
@@ -37,6 +40,56 @@ interface PendingOp {
|
|
|
37
40
|
fn: string;
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
export interface Interval {
|
|
44
|
+
startUs: number;
|
|
45
|
+
endUs: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Merge overlapping or adjacent intervals. Returns a new sorted array
|
|
50
|
+
* of non-overlapping intervals.
|
|
51
|
+
*/
|
|
52
|
+
export function mergeIntervals(intervals: Interval[]): Interval[] {
|
|
53
|
+
if (intervals.length <= 1) return intervals.slice();
|
|
54
|
+
|
|
55
|
+
const sorted = intervals.slice().sort((a, b) => a.startUs - b.startUs);
|
|
56
|
+
const merged: Interval[] = [{ ...sorted[0]! }];
|
|
57
|
+
|
|
58
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
59
|
+
const current = sorted[i]!;
|
|
60
|
+
const last = merged[merged.length - 1]!;
|
|
61
|
+
|
|
62
|
+
if (current.startUs <= last.endUs) {
|
|
63
|
+
// Overlapping or adjacent — extend
|
|
64
|
+
if (current.endUs > last.endUs) {
|
|
65
|
+
last.endUs = current.endUs;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
merged.push({ ...current });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return merged;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sum the durations of a list of (presumably non-overlapping) intervals.
|
|
77
|
+
*/
|
|
78
|
+
function sumIntervals(intervals: Interval[]): number {
|
|
79
|
+
let total = 0;
|
|
80
|
+
for (const iv of intervals) {
|
|
81
|
+
total += iv.endUs - iv.startUs;
|
|
82
|
+
}
|
|
83
|
+
return total;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert an hrtime tuple to absolute microseconds.
|
|
88
|
+
*/
|
|
89
|
+
function hrtimeToUs(hr: [number, number]): number {
|
|
90
|
+
return hr[0] * 1_000_000 + Math.round(hr[1] / 1000);
|
|
91
|
+
}
|
|
92
|
+
|
|
40
93
|
/**
|
|
41
94
|
* Parse a single line from an Error().stack trace into file path and function id.
|
|
42
95
|
* Returns null for lines that don't match V8's stack frame format or are node internals.
|
|
@@ -68,25 +121,40 @@ export function parseStackLine(line: string): { filePath: string; functionId: st
|
|
|
68
121
|
|
|
69
122
|
export class AsyncTracker {
|
|
70
123
|
private readonly resolver: PackageResolver;
|
|
71
|
-
private readonly store: SampleStore;
|
|
72
124
|
private readonly thresholdUs: number;
|
|
73
125
|
private hook: AsyncHook | null = null;
|
|
74
126
|
private pending = new Map<number, PendingOp>();
|
|
75
127
|
|
|
128
|
+
/** Buffered intervals keyed by "pkg\0file\0fn" */
|
|
129
|
+
private keyedIntervals = new Map<string, Interval[]>();
|
|
130
|
+
/** Flat list of all intervals for global merging */
|
|
131
|
+
private globalIntervals: Interval[] = [];
|
|
132
|
+
/** Origin time in absolute microseconds, set when enable() is called */
|
|
133
|
+
private originUs = 0;
|
|
134
|
+
|
|
135
|
+
/** Merged global total set after flush() */
|
|
136
|
+
private _mergedTotalUs = 0;
|
|
137
|
+
|
|
76
138
|
/**
|
|
77
139
|
* @param resolver - PackageResolver for mapping file paths to packages
|
|
78
|
-
* @param store - SampleStore to record async wait times into
|
|
140
|
+
* @param store - SampleStore to record async wait times into (used at flush time)
|
|
79
141
|
* @param thresholdUs - Minimum wait duration in microseconds to record (default 1000 = 1ms)
|
|
80
142
|
*/
|
|
81
|
-
constructor(resolver: PackageResolver, store: SampleStore, thresholdUs: number = 1000) {
|
|
143
|
+
constructor(resolver: PackageResolver, private readonly store: SampleStore, thresholdUs: number = 1000) {
|
|
82
144
|
this.resolver = resolver;
|
|
83
|
-
this.store = store;
|
|
84
145
|
this.thresholdUs = thresholdUs;
|
|
85
146
|
}
|
|
86
147
|
|
|
148
|
+
/** Merged global async total in microseconds, available after disable(). */
|
|
149
|
+
get mergedTotalUs(): number {
|
|
150
|
+
return this._mergedTotalUs;
|
|
151
|
+
}
|
|
152
|
+
|
|
87
153
|
enable(): void {
|
|
88
154
|
if (this.hook) return;
|
|
89
155
|
|
|
156
|
+
this.originUs = hrtimeToUs(process.hrtime());
|
|
157
|
+
|
|
90
158
|
this.hook = createHook({
|
|
91
159
|
init: (asyncId: number, type: string) => {
|
|
92
160
|
if (!TRACKED_TYPES.has(type)) return;
|
|
@@ -131,11 +199,23 @@ export class AsyncTracker {
|
|
|
131
199
|
const op = this.pending.get(asyncId);
|
|
132
200
|
if (!op) return;
|
|
133
201
|
|
|
134
|
-
const
|
|
135
|
-
const
|
|
202
|
+
const endHr = process.hrtime();
|
|
203
|
+
const startUs = hrtimeToUs(op.startHrtime);
|
|
204
|
+
const endUs = hrtimeToUs(endHr);
|
|
205
|
+
const durationUs = endUs - startUs;
|
|
136
206
|
|
|
137
207
|
if (durationUs >= this.thresholdUs) {
|
|
138
|
-
|
|
208
|
+
const interval: Interval = { startUs, endUs };
|
|
209
|
+
const key = `${op.pkg}\0${op.file}\0${op.fn}`;
|
|
210
|
+
|
|
211
|
+
let arr = this.keyedIntervals.get(key);
|
|
212
|
+
if (!arr) {
|
|
213
|
+
arr = [];
|
|
214
|
+
this.keyedIntervals.set(key, arr);
|
|
215
|
+
}
|
|
216
|
+
arr.push(interval);
|
|
217
|
+
|
|
218
|
+
this.globalIntervals.push(interval);
|
|
139
219
|
}
|
|
140
220
|
|
|
141
221
|
this.pending.delete(asyncId);
|
|
@@ -156,23 +236,54 @@ export class AsyncTracker {
|
|
|
156
236
|
this.hook.disable();
|
|
157
237
|
|
|
158
238
|
// Resolve any pending ops using current time
|
|
159
|
-
const
|
|
239
|
+
const nowHr = process.hrtime();
|
|
240
|
+
const nowUs = hrtimeToUs(nowHr);
|
|
160
241
|
for (const [, op] of this.pending) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
let nanos = now[1] - op.startHrtime[1];
|
|
164
|
-
if (nanos < 0) {
|
|
165
|
-
secs -= 1;
|
|
166
|
-
nanos += 1_000_000_000;
|
|
167
|
-
}
|
|
168
|
-
const durationUs = secs * 1_000_000 + Math.round(nanos / 1000);
|
|
242
|
+
const startUs = hrtimeToUs(op.startHrtime);
|
|
243
|
+
const durationUs = nowUs - startUs;
|
|
169
244
|
|
|
170
245
|
if (durationUs >= this.thresholdUs) {
|
|
171
|
-
|
|
246
|
+
const interval: Interval = { startUs, endUs: nowUs };
|
|
247
|
+
const key = `${op.pkg}\0${op.file}\0${op.fn}`;
|
|
248
|
+
|
|
249
|
+
let arr = this.keyedIntervals.get(key);
|
|
250
|
+
if (!arr) {
|
|
251
|
+
arr = [];
|
|
252
|
+
this.keyedIntervals.set(key, arr);
|
|
253
|
+
}
|
|
254
|
+
arr.push(interval);
|
|
255
|
+
|
|
256
|
+
this.globalIntervals.push(interval);
|
|
172
257
|
}
|
|
173
258
|
}
|
|
174
259
|
|
|
175
260
|
this.pending.clear();
|
|
176
261
|
this.hook = null;
|
|
262
|
+
|
|
263
|
+
this.flush();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Merge buffered intervals and record to the store.
|
|
268
|
+
* Sets mergedTotalUs to the global merged duration.
|
|
269
|
+
*/
|
|
270
|
+
private flush(): void {
|
|
271
|
+
// Per-key: merge overlapping intervals, sum durations, record to store
|
|
272
|
+
for (const [key, intervals] of this.keyedIntervals) {
|
|
273
|
+
const merged = mergeIntervals(intervals);
|
|
274
|
+
const totalUs = sumIntervals(merged);
|
|
275
|
+
if (totalUs > 0) {
|
|
276
|
+
const parts = key.split('\0');
|
|
277
|
+
this.store.record(parts[0]!, parts[1]!, parts[2]!, totalUs);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Global: merge all intervals to compute real elapsed async wait
|
|
282
|
+
const globalMerged = mergeIntervals(this.globalIntervals);
|
|
283
|
+
this._mergedTotalUs = sumIntervals(globalMerged);
|
|
284
|
+
|
|
285
|
+
// Clean up buffers
|
|
286
|
+
this.keyedIntervals.clear();
|
|
287
|
+
this.globalIntervals = [];
|
|
177
288
|
}
|
|
178
289
|
}
|
package/src/pkg-profile.ts
CHANGED
|
@@ -31,6 +31,8 @@ export class PkgProfile {
|
|
|
31
31
|
readonly projectName: string;
|
|
32
32
|
/** Total async wait time in microseconds (undefined when async tracking not enabled) */
|
|
33
33
|
readonly totalAsyncTimeUs?: number;
|
|
34
|
+
/** Elapsed wall time in microseconds from start() to stop() */
|
|
35
|
+
readonly wallTimeUs?: number;
|
|
34
36
|
|
|
35
37
|
/** @internal */
|
|
36
38
|
constructor(data: ReportData) {
|
|
@@ -40,6 +42,7 @@ export class PkgProfile {
|
|
|
40
42
|
this.otherCount = data.otherCount;
|
|
41
43
|
this.projectName = data.projectName;
|
|
42
44
|
this.totalAsyncTimeUs = data.totalAsyncTimeUs;
|
|
45
|
+
this.wallTimeUs = data.wallTimeUs;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/**
|
|
@@ -56,6 +59,7 @@ export class PkgProfile {
|
|
|
56
59
|
otherCount: this.otherCount,
|
|
57
60
|
projectName: this.projectName,
|
|
58
61
|
totalAsyncTimeUs: this.totalAsyncTimeUs,
|
|
62
|
+
wallTimeUs: this.wallTimeUs,
|
|
59
63
|
};
|
|
60
64
|
const html = renderHtml(data);
|
|
61
65
|
|