Initial Commit
This commit is contained in:
45
openspec/archive/2026-05-24-profiler/proposal.md
Normal file
45
openspec/archive/2026-05-24-profiler/proposal.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Proposal — Migrate @suaveplan/profiler to genesis
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/profiler` is the universal-tier runtime performance profiling primitive consumed by downstream observability tooling. The upstream package at `/mnt/s/web/suaveplan/packages/universal/profiler/` reached 93.95% test coverage and lacks the genesis enterprise gates (`Clock`/`TimersLike`/`LoggerLike` injection, `defineCodes` error registry, OTel meter wiring, Zod-validated boundaries, 100/100/100/100 coverage). This change re-lands the package at `packages/devtools/tooling/profiler/` with the full feature surface and the genesis enterprise contract.
|
||||
|
||||
## What
|
||||
|
||||
Land `@suaveplan/profiler@0.1.0` with the following modules (one per feature folder, 5-files-per-feature):
|
||||
|
||||
- `profiler/` — `Profiler` class: `start`/`stop`/`mark`/`measure`/`measureAsync`
|
||||
- `sampler/` — periodic metric sampler with injectable `Clock`+`TimersLike`
|
||||
- `memory/` — heap snapshot capture with injectable capture-fn; leak detection
|
||||
- `bottleneck/` — hot-path detector + budget-violation finder
|
||||
- `comparison/` — diff two profiles; regression/improvement classifier
|
||||
- `flamegraph/` — flamegraph JSON tree builder + recursive-call collapse
|
||||
- `reporter/` — text formatter + percentile statistics
|
||||
- `otel/` — OTLP-compatible span exporter (HTTP `fetch`)
|
||||
- `error-codes/` — `PROFILER_CODES` via `defineCodes` + `ProfilerError`
|
||||
- `telemetry/` — OTel meter for the profiler itself (counter + histogram)
|
||||
- `types/` — shared `ProfileEntry`/`ProfileMark`/`SamplePoint`/etc
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Upstream file | Genesis file | Notes |
|
||||
| --- | --- | --- |
|
||||
| `src/profiler/profiler.ts` | `src/profiler/profiler.ts` | `Clock`+`TimersLike` injected; Zod options |
|
||||
| `src/sampler/sampler.ts` | `src/sampler/sampler.ts` | `TimersLike.setTimer` replaces `setInterval` |
|
||||
| `src/memory/memory-profiler.ts` | `src/memory/memory.ts` | Injectable `HeapCapture` fn replaces global probe |
|
||||
| `src/bottleneck/bottleneck-detector.ts` | `src/bottleneck/bottleneck.ts` | Same algorithm, renamed file |
|
||||
| `src/comparison/profile-comparator.ts` | `src/comparison/comparison.ts` | Same algorithm |
|
||||
| `src/flamegraph/flamegraph-generator.ts` | `src/flamegraph/flamegraph.ts` | Same algorithm |
|
||||
| `src/reporter/reporter.ts` | `src/reporter/reporter.ts` | Same algorithm |
|
||||
| `src/otel/otel-exporter.ts` | `src/otel/otel.ts` | `fetch` is injectable for tests |
|
||||
| `src/index.ts` | `src/index.ts` | Explicit named re-exports only |
|
||||
| (none) | `src/error-codes/error-codes.ts` | New — required by genesis rule |
|
||||
| (none) | `src/telemetry/telemetry.ts` | New — required by genesis rule |
|
||||
|
||||
## Deviations from upstream
|
||||
|
||||
1. Upstream uses `@suaveplan/events` `EventEmitter` with variadic tuple payloads. Genesis `@suaveplan/events` exposes `EventBus` with single-payload (`emit(type, payload)`) — event maps reshaped to single-object payloads.
|
||||
2. Upstream calls `Date.now()` and `setInterval` directly. Genesis injects `Clock` + `TimersLike` per the universal-tier contract.
|
||||
3. Upstream has no error-code registry. Genesis adds `PROFILER_CODES`/`ProfilerError`.
|
||||
4. Upstream emits no profiler-self-telemetry. Genesis adds a `telemetry/` module emitting per-public-call counters + duration histograms.
|
||||
5. Boundary validation: every public constructor option object is now Zod-validated.
|
||||
77
openspec/archive/2026-05-24-profiler/specs/profiler/spec.md
Normal file
77
openspec/archive/2026-05-24-profiler/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`).
|
||||
55
openspec/archive/2026-05-24-profiler/tasks.md
Normal file
55
openspec/archive/2026-05-24-profiler/tasks.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Tasks — Migrate @suaveplan/profiler to genesis
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 `git status` clean inside `.claude/worktrees/agent-a53dffba816b6eb95`
|
||||
- [x] 0.2 Account for every worktree returned by `git worktree list` (this agent's worktree + main checkout)
|
||||
- [x] 0.3 Confirm `MANIFEST.md` package count matches `find packages -maxdepth 3 -name package.json`
|
||||
- [x] 0.4 §0a Sync: `git merge-base --is-ancestor 56c1fae5078a5afff119847b4b96d86c78668f02 HEAD` reports true after `git fetch /home/paul/web/genesis genesis && git merge FETCH_HEAD --no-edit`
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/profiler.md` end-to-end
|
||||
- [x] 1.2 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.3 Draft `proposal.md` with `## Source mapping` table
|
||||
- [x] 1.4 Draft `specs/profiler/spec.md` with `## ADDED Requirements` and `**Verified by:**` lines
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 `src/types/` — shared types module
|
||||
- [x] 2.2 `src/error-codes/` — `PROFILER_CODES` registry + `ProfilerError`
|
||||
- [x] 2.3 `src/telemetry/` — OTel meter, counters, histograms
|
||||
- [x] 2.4 `src/profiler/` — `Profiler` class with `Clock`-injection
|
||||
- [x] 2.5 `src/sampler/` — `Sampler` with `TimersLike` + injectable memory-probe
|
||||
- [x] 2.6 `src/memory/` — `MemoryProfiler` with injectable `HeapCapture`
|
||||
- [x] 2.7 `src/bottleneck/` — `BottleneckDetector`
|
||||
- [x] 2.8 `src/comparison/` — `ProfileComparator`
|
||||
- [x] 2.9 `src/flamegraph/` — `FlameGraphGenerator`
|
||||
- [x] 2.10 `src/reporter/` — `ProfileReporter`
|
||||
- [x] 2.11 `src/otel/` — `OtelExporter` with injectable `fetch` + crypto-rng
|
||||
- [x] 2.12 `src/index.ts` — explicit named re-exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Unit tests at `src/<feature>/<feature>.test.ts` for every module
|
||||
- [x] 3.2 100/100/100/100 coverage gate met (istanbul)
|
||||
- [x] 3.3 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
- [x] 3.4 All tests import from `@suaveplan/testing/runner`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` for every implementation `.ts` (≥200 words, basic + advanced)
|
||||
- [x] 4.2 `README.md` at package root
|
||||
|
||||
## 5. Pre-archive verification
|
||||
|
||||
- [ ] 5.1 `bunx turbo lint typecheck test --filter=@suaveplan/profiler` green
|
||||
- [ ] 5.2 `bunx turbo test:coverage --filter=@suaveplan/profiler` 100/100/100/100
|
||||
- [ ] 5.3 `bunx turbo build --filter=@suaveplan/profiler` green
|
||||
- [ ] 5.4 `bun scripts/lint/check-feature-layout.ts | grep profiler` — empty (no violations)
|
||||
- [ ] 5.5 `bun scripts/folder-reorg/generate-manifest.ts` refreshes MANIFEST.md
|
||||
|
||||
## 6. Archive (post-merge — orchestrator's job)
|
||||
|
||||
- [ ] 6.1 Fold spec delta into `openspec/specs/profiler/spec.md`
|
||||
- [ ] 6.2 Move `openspec/changes/profiler/` → `openspec/archive/<date>-profiler/`
|
||||
Reference in New Issue
Block a user