# Profiler — Spec Delta ## ADDED Requirements ### Requirement: Profiler captures named measurements with injectable clock The `Profiler` class SHALL expose `start(label, category?, metadata?)`, `stop(label)`, `mark(label, metadata?)`, `measure(label, fn, category?)`, and `measureAsync(label, fn, category?)`. Wall-clock readings MUST come from an injectable `Clock`; the default MUST be `() => Date.now()` (or `performance.now()` when available — the constructor accepts a `Clock` override). `measure` and `measureAsync` MUST call `stop` even when the wrapped function throws. **Verified by:** `profiler.test.ts` › "start + stop records an entry with duration", "measure wraps a sync fn and propagates throws", "measureAsync wraps an async fn and propagates throws", "uses the injected clock for timing". ### Requirement: Sampler runs on TimersLike, not setInterval The `Sampler` class SHALL drive its periodic sampling through an injected `TimersLike.setTimer` and SHALL stop cleanly by calling `TimersLike.clearTimer`. Calling `start()` while already running MUST be a no-op; calling `stop()` while not running MUST be a no-op. Samples MUST be capped at `maxSamples` via FIFO eviction. **Verified by:** `sampler.test.ts` › "start schedules samples via TimersLike", "stop clears the active timer", "double-start is a no-op", "double-stop is a no-op", "evicts oldest samples beyond maxSamples". ### Requirement: MemoryProfiler accepts injectable HeapCapture The `MemoryProfiler` SHALL accept an injectable `HeapCapture` function returning `{ heapUsed, heapTotal }`. The default capture function MUST read `process.memoryUsage()` when available, fall back to `performance.memory` (Chrome), and return zeros otherwise. Leak detection SHALL emit `memory:leak-detected` when the last `leakWindowSize` snapshots show monotonic non-decreasing growth ≥ `leakThresholdBytes`. **Verified by:** `memory.test.ts` › "uses the injected heap-capture", "default capture handles missing process and performance.memory", "detectLeak fires when growth exceeds threshold", "detectLeak ignores stable windows". ### Requirement: PROFILER_CODES registry via defineCodes The package MUST publish a stable `PROFILER_CODES` registry built with `defineCodes("PROFILER", [...])` and an accompanying `ProfilerError extends SuaveplanError` class with a public constructor delegating to `super`. Every public option-validation failure MUST throw `ProfilerError` with a code from the registry. **Verified by:** `error-codes.test.ts` › "every code is namespaced under PROFILER.", "ProfilerError carries the supplied code and message", "Profiler throws INVALID_OPTIONS for non-positive percentiles", "Sampler throws INVALID_OPTIONS for non-positive intervalMs". ### Requirement: Telemetry instruments emit on public calls The package MUST register an OTel meter at `@suaveplan/profiler` exposing: - `profiler_entries_total` counter — incremented on every completed `stop()` - `profiler_marks_total` counter — incremented on every `mark()` - `profiler_measure_duration_ms` histogram — recorded on every completed `stop()` - `profiler_samples_total` counter — incremented on every `takeSample()` - `profiler_memory_snapshots_total` counter — incremented on every `takeSnapshot()` A real in-memory `MeterProvider` registered before module import MUST see metric points after public calls. **Verified by:** `telemetry.test.ts` › "records entries on stop", "records marks", "records samples", "records snapshots", "records measure duration". ### Requirement: BottleneckDetector ranks hot spots deterministically `BottleneckDetector.analyze` MUST return up to `maxResults` bottlenecks sorted by `impact = avgDuration * frequency` descending. `findHotSpots` MUST return entries that exceed `timeBudgetMs` and occur ≥ `hotSpotFrequency` times. `findBudgetViolations` MUST return entries whose `duration` exceeds `timeBudgetMs`. **Verified by:** `bottleneck.test.ts` › "analyze ranks by impact", "findHotSpots requires both threshold + frequency", "findBudgetViolations returns over-budget entries", "respects maxResults cap". ### Requirement: ProfileComparator classifies deltas by regressionThreshold `ProfileComparator` MUST classify each per-label delta into regressions, improvements, or unchanged based on `percentChange` vs `regressionThreshold`. Regressions MUST be sorted by `percentChange` descending; improvements ascending. Both `compareReports` (ProfileReport pair) and `compareEntries` (entry-array pair) MUST be supported. **Verified by:** `comparison.test.ts` › "classifies regressions and improvements", "computeReports works at category granularity", "handles zero-baseline edge case", "sorts regressions descending and improvements ascending". ### Requirement: FlameGraphGenerator builds a containment tree `FlameGraphGenerator.generate(entries)` MUST return a root `FlameNode` whose children are populated by start/end-time containment. `selfTime` MUST equal `value` minus the sum of immediate children's `value`. `collapseRecursive` MUST collapse consecutive same-name parent/child nodes. `toD3Format` MUST emit `{ name, value, children }` JSON. **Verified by:** `flamegraph.test.ts` › "builds a containment tree", "computes selfTime as value minus children", "collapses recursive nodes", "toD3Format emits d3-compatible JSON". ### Requirement: ProfileReporter computes percentile statistics `ProfileReporter.generateReport` MUST return `count`, `min`, `max`, `mean`, `median`, configurable `percentiles`, total duration, per-category stats, alerts above `alertThreshold`, and a copy of marks. Empty-input MUST return a zero-stats object. **Verified by:** `reporter.test.ts` › "computes percentiles", "handles empty entries", "groups by category", "flags alerts above threshold", "formatReport produces stable text output". ### Requirement: OtelExporter emits OTLP-compatible JSON with injectable fetch + rng `OtelExporter` MUST convert entries to OTLP-compatible spans with `traceId` (16 bytes hex) and `spanId` (8 bytes hex). A `Fetch` function and `Rng` (random-bytes) MUST be injectable. `export(spans)` MUST return `true` on `response.ok`, `false` otherwise (including thrown errors). `exportBatch()` MUST chunk pending spans into `batchSize` slices. **Verified by:** `otel.test.ts` › "converts entries to OTLP spans", "respects injected fetch", "returns false on non-ok response", "returns false on fetch throw", "exportBatch chunks pending spans". ### Requirement: Sibling .md per implementation .ts Every non-barrel, non-test implementation `.ts` file MUST have a sibling `.md` file with the six required sections (Purpose, Features, Usage: Basic, Usage: Advanced, API Reference, Implementation Notes), ≥200 words excluding code. **Verified by:** `bun run docs:check` at the package level (script is wired in `package.json`).