6.7 KiB
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<ProfilerCode> 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_totalcounter — incremented on every completedstop()profiler_marks_totalcounter — incremented on everymark()profiler_measure_duration_mshistogram — recorded on every completedstop()profiler_samples_totalcounter — incremented on everytakeSample()profiler_memory_snapshots_totalcounter — incremented on everytakeSnapshot()
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).