Files
suaveplan/openspec/archive/2026-05-24-profiler/specs/profiler/spec.md
2026-05-27 23:29:53 +01:00

6.7 KiB
Raw Blame History

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_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).