Initial Commit
This commit is contained in:
77
openspec/specs/profiler/spec.md
Normal file
77
openspec/specs/profiler/spec.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# 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`).
|
||||
Reference in New Issue
Block a user