Initial Commit
This commit is contained in:
240
openspec/changes/enterprise-profiler/proposal.md
Normal file
240
openspec/changes/enterprise-profiler/proposal.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# OpenSpec Change: enterprise-profiler
|
||||
|
||||
- **Change ID**: enterprise-profiler
|
||||
- **Package**: `@suaveplan/profiler`
|
||||
- **Category**: universal/platform
|
||||
- **Target path**: `universal/platform/profiler`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/profiler.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/profiler` on the genesis branch from scratch at enterprise quality. The package provides runtime performance profiling primitives: CPU/wall-time sampling, memory snapshots, flame-graph serialization, profile sessions with start/stop/export, and a plugin interface for sink adapters (console, file, remote). Genesis adds structured errors via `@suaveplan/error`, Zod schemas for all profile types, OTel integration (profiles forwarded as OTel log records), `AbortSignal` on async session exports, injectable `Clock` and `ProcessSampler` for deterministic testing, and property-based tests for statistical aggregation.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | Base class for `ProfilerError`; stable error codes |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` injectable interfaces |
|
||||
| `@suaveplan/events` (`workspace:^`) | Event bus for profile-session lifecycle events |
|
||||
| `@suaveplan/logger` (`workspace:^`) | Structured logging with OTel trace correlation |
|
||||
| `zod` (`catalog:forms`) | Zod schemas for all public profile types |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on session start/stop/export; profiler telemetry |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`).
|
||||
|
||||
---
|
||||
|
||||
## Public API surface
|
||||
|
||||
### Core types
|
||||
|
||||
```ts
|
||||
interface ProfileSample {
|
||||
readonly timestamp: number
|
||||
readonly stackFrames: ReadonlyArray<StackFrame>
|
||||
readonly cpuUsage?: number
|
||||
readonly heapUsed?: number
|
||||
readonly heapTotal?: number
|
||||
readonly externalMemory?: number
|
||||
}
|
||||
|
||||
interface StackFrame {
|
||||
readonly functionName: string
|
||||
readonly scriptId?: string
|
||||
readonly url?: string
|
||||
readonly lineNumber?: number
|
||||
readonly columnNumber?: number
|
||||
}
|
||||
|
||||
interface ProfileSession {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly startTime: number
|
||||
readonly endTime?: number
|
||||
readonly samples: ReadonlyArray<ProfileSample>
|
||||
}
|
||||
|
||||
interface FlamegraphNode {
|
||||
readonly name: string
|
||||
readonly value: number
|
||||
readonly children: ReadonlyArray<FlamegraphNode>
|
||||
}
|
||||
```
|
||||
|
||||
### Profiler API
|
||||
|
||||
```ts
|
||||
function createProfiler(options?: ProfilerOptions): Profiler
|
||||
|
||||
interface Profiler {
|
||||
startSession(name: string, options?: SessionOptions): ProfilerSession
|
||||
getSession(id: string): ProfilerSession | null
|
||||
listSessions(): ReadonlyArray<ProfileSession>
|
||||
clearSessions(): void
|
||||
use(sink: ProfilerSink): this
|
||||
}
|
||||
|
||||
interface ProfilerSession {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
stop(): ProfileSession
|
||||
sample(): ProfileSample
|
||||
export(format: 'json' | 'flamegraph', signal?: AbortSignal): Promise<string>
|
||||
toFlamegraph(): FlamegraphNode
|
||||
}
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```ts
|
||||
interface ProfilerOptions {
|
||||
sampleIntervalMs?: number
|
||||
maxSamples?: number
|
||||
collectMemory?: boolean
|
||||
collectCpu?: boolean
|
||||
clock?: Clock
|
||||
logger?: LoggerLike
|
||||
eventBus?: EventBusLike
|
||||
}
|
||||
|
||||
interface SessionOptions {
|
||||
tags?: Record<string, string>
|
||||
autoStop?: boolean
|
||||
autoStopAfterMs?: number
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin/sink interface
|
||||
|
||||
```ts
|
||||
interface ProfilerSink {
|
||||
readonly name: string
|
||||
onSessionEnd(session: ProfileSession): void | Promise<void>
|
||||
}
|
||||
|
||||
function defineProfilerSink(sink: ProfilerSink): ProfilerSink
|
||||
function createConsoleSink(options?: ConsoleSinkOptions): ProfilerSink
|
||||
function createJsonSink(options: JsonSinkOptions): ProfilerSink
|
||||
```
|
||||
|
||||
### Utilities
|
||||
|
||||
```ts
|
||||
function aggregateSamples(samples: ProfileSample[]): ProfileAggregate
|
||||
function buildFlamegraph(session: ProfileSession): FlamegraphNode
|
||||
function formatDuration(ms: number): string
|
||||
|
||||
interface ProfileAggregate {
|
||||
readonly count: number
|
||||
readonly totalCpuMs: number
|
||||
readonly avgCpuMs: number
|
||||
readonly p50CpuMs: number
|
||||
readonly p95CpuMs: number
|
||||
readonly p99CpuMs: number
|
||||
readonly peakHeapMb: number
|
||||
readonly avgHeapMb: number
|
||||
}
|
||||
```
|
||||
|
||||
### Error codes
|
||||
|
||||
```ts
|
||||
type ProfilerErrorCode =
|
||||
| 'PROFILER_SESSION_NOT_FOUND'
|
||||
| 'PROFILER_SESSION_ALREADY_STOPPED'
|
||||
| 'PROFILER_EXPORT_FAILED'
|
||||
| 'PROFILER_SAMPLE_FAILED'
|
||||
| 'PROFILER_SINK_FAILED'
|
||||
|
||||
function registerProfilerErrorCodes(): void
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Genesis additions
|
||||
|
||||
1. **Structured `ProfilerError`** extending `SuaveplanError` with stable `PROFILER.*` codes
|
||||
2. **Injectable `Clock`** so sample timestamps are deterministic in tests
|
||||
3. **`ProfilerSink` plugin interface** — `use()` on `Profiler`; console and JSON sinks built-in
|
||||
4. **`AbortSignal` on `session.export()`**
|
||||
5. **OTel integration** — span `profiler.session` with attributes `session.name`, `session.sample_count`; histogram `profiler.session_duration_ms`
|
||||
6. **`aggregateSamples` with percentile stats** (p50/p95/p99) computed via insertion sort on small sample sets
|
||||
7. **Property-based tests** for `aggregateSamples` statistical invariants, `buildFlamegraph` tree structure
|
||||
8. **Benchmark suite** — `sample()` hot path, `buildFlamegraph` on 1k/10k sample sessions
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
universal/platform/profiler/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
error-codes.ts
|
||||
constants/
|
||||
index.ts
|
||||
types/
|
||||
common.ts
|
||||
common.md
|
||||
core/
|
||||
profiler/
|
||||
profiler.ts
|
||||
profiler.md
|
||||
profiler.types.ts
|
||||
profiler.test.ts
|
||||
index.ts
|
||||
session/
|
||||
session.ts
|
||||
session.md
|
||||
session.types.ts
|
||||
session.test.ts
|
||||
index.ts
|
||||
sinks/
|
||||
console-sink/
|
||||
console-sink.ts
|
||||
console-sink.md
|
||||
console-sink.test.ts
|
||||
index.ts
|
||||
json-sink/
|
||||
json-sink.ts
|
||||
json-sink.md
|
||||
json-sink.test.ts
|
||||
index.ts
|
||||
utils/
|
||||
aggregate.ts
|
||||
aggregate.md
|
||||
aggregate.test.ts
|
||||
flamegraph.ts
|
||||
flamegraph.md
|
||||
flamegraph.test.ts
|
||||
__benchmarks__/
|
||||
profiler.bench.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
property.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Downstream packages**: Any package needing runtime performance profiling. No existing genesis package currently depends on it.
|
||||
|
||||
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger` must be published to Verdaccio.
|
||||
131
openspec/changes/enterprise-profiler/tasks.md
Normal file
131
openspec/changes/enterprise-profiler/tasks.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Tasks: enterprise-profiler
|
||||
|
||||
**Change ID**: enterprise-profiler
|
||||
**Package**: `@suaveplan/profiler`
|
||||
**Target path**: `universal/platform/profiler`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`
|
||||
|
||||
---
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] Create `universal/platform/profiler/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`.
|
||||
- [ ] Write `package.json` at version `0.1.0`:
|
||||
- `"type": "module"`, `"sideEffects": false`
|
||||
- `exports` map: `"."` barrel + `"./core/profiler/profiler"`, `"./core/session/session"`, `"./sinks/console-sink/console-sink"`, `"./sinks/json-sink/json-sink"`, `"./utils/aggregate"`, `"./utils/flamegraph"`, `"./types/common"`
|
||||
- `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/events workspace:^`, `@suaveplan/logger workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`
|
||||
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `mitata catalog:testing`, `@suaveplan/testing workspace:^`
|
||||
- [ ] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
|
||||
- [ ] Write `vitest.config.ts` using `createTestConfig()`.
|
||||
- [ ] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench` scripts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types and error codes
|
||||
|
||||
- [ ] Define all public types in `src/types/common.ts`: `ProfileSample`, `StackFrame`, `ProfileSession`, `FlamegraphNode`, `ProfileAggregate`, `ProfilerOptions`, `SessionOptions`, `ProfilerSink`, `ConsoleSinkOptions`, `JsonSinkOptions`.
|
||||
- [ ] Write `src/types/common.md` (≥200 words, simple + advanced examples).
|
||||
- [ ] Define error codes in `src/error-codes.ts`: `ProfilerErrorCode` union, metadata, `registerProfilerErrorCodes()`.
|
||||
- [ ] Write Zod schemas in `src/types/schemas.ts` + `src/types/schemas.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Aggregate utilities (`src/utils/aggregate.ts`)
|
||||
|
||||
- [ ] Implement `aggregateSamples(samples)`: compute count, totalCpuMs, avgCpuMs, p50/p95/p99 (insertion-sort on the cpu array), peakHeapMb, avgHeapMb.
|
||||
- [ ] Implement `formatDuration(ms)`: format as `Xms`, `Xs`, `Xm Xs`.
|
||||
- [ ] Write `aggregate.test.ts` (≤500 lines): empty array, single sample, percentile correctness for known data.
|
||||
- [ ] Write `aggregate.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 4. Flamegraph utilities (`src/utils/flamegraph.ts`)
|
||||
|
||||
- [ ] Implement `buildFlamegraph(session)`: group samples by stack frame path, build tree by folding frames into children.
|
||||
- [ ] Each node's `value` = number of samples that include this frame.
|
||||
- [ ] Produce sorted children (descending by value).
|
||||
- [ ] Write `flamegraph.test.ts` (≤500 lines): empty session, single frame, multi-frame stacks, shared frames merge correctly.
|
||||
- [ ] Write `flamegraph.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 5. Profiler session (`src/core/session/`)
|
||||
|
||||
- [ ] Implement `ProfilerSession` (internal class):
|
||||
- `sample()` — captures `clock.now()` + heap stats via `process.memoryUsage()` (guarded for non-Node runtimes); returns `ProfileSample`.
|
||||
- `stop()` — sets `endTime`, returns immutable `ProfileSession` snapshot.
|
||||
- `export('json', signal?)` — JSON-serialises session.
|
||||
- `export('flamegraph', signal?)` — calls `buildFlamegraph`, JSON-serialises.
|
||||
- `toFlamegraph()` — synchronous `buildFlamegraph` call.
|
||||
- [ ] Write `session.test.ts` (≤500 lines): sample, stop, export json, export flamegraph, abort on export.
|
||||
- [ ] Write `session.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 6. Profiler (`src/core/profiler/`)
|
||||
|
||||
- [ ] Implement `createProfiler(options?)`: validates options with Zod; stores sessions in `Map<string, ProfilerSession>`.
|
||||
- [ ] Implement `profiler.startSession(name, options?)` — generates UUID id, stores session, emits OTel span `profiler.session.start`.
|
||||
- [ ] Implement `profiler.getSession(id)`, `listSessions()`, `clearSessions()`.
|
||||
- [ ] Implement `profiler.use(sink)` — registers sink; on session stop, calls `sink.onSessionEnd(session)` asynchronously.
|
||||
- [ ] On session stop, emit OTel span `profiler.session.stop`, histogram `profiler.session_duration_ms`.
|
||||
- [ ] Write `profiler.test.ts` (≤500 lines): session lifecycle, sink called on stop, multiple sessions.
|
||||
- [ ] Write `profiler.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 7. Sinks (`src/sinks/`)
|
||||
|
||||
### 7a. Console sink
|
||||
|
||||
- [ ] Implement `createConsoleSink(options?)` — on session end, prints a summary table via `console.log` with aggregate stats.
|
||||
- [ ] Write `console-sink.test.ts`: output contains session name and p95 duration.
|
||||
- [ ] Write `console-sink.md`.
|
||||
|
||||
### 7b. JSON sink
|
||||
|
||||
- [ ] Implement `createJsonSink(options: { path: string })` — on session end, appends JSON line to file via `Bun.write` (or `fs.appendFile` fallback).
|
||||
- [ ] Write `json-sink.test.ts`: file written, valid JSON, append-safe.
|
||||
- [ ] Write `json-sink.md`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Property-based tests (`tests/property.test.ts`)
|
||||
|
||||
- [ ] `aggregateSamples` invariants: avg ≤ max, p50 ≤ p95 ≤ p99, count equals input length.
|
||||
- [ ] `buildFlamegraph` node value sum: root node value ≤ total samples.
|
||||
- [ ] Session stop idempotency: calling `stop()` twice on a session should be a no-op after the first call (endTime does not change).
|
||||
|
||||
---
|
||||
|
||||
## 9. Benchmarks (`src/__benchmarks__/profiler.bench.ts`)
|
||||
|
||||
- [ ] `sample_hot`: 1000 `session.sample()` calls.
|
||||
- [ ] `buildFlamegraph_1k`, `buildFlamegraph_10k`: flamegraph on 1k and 10k sample sessions.
|
||||
- [ ] `aggregateSamples_1k`, `aggregateSamples_10k`: aggregate on 1k and 10k samples.
|
||||
|
||||
---
|
||||
|
||||
## 10. Docs
|
||||
|
||||
- [ ] All per-file `.md` files per layout in `proposal.md`.
|
||||
- [ ] `universal/platform/profiler/README.md`: purpose, installation, quick-start, API section, dependency table, benchmarks table.
|
||||
|
||||
---
|
||||
|
||||
## 11. Verify and publish
|
||||
|
||||
- [ ] `bunx turbo lint --filter=@suaveplan/profiler` — zero errors.
|
||||
- [ ] `bunx turbo typecheck --filter=@suaveplan/profiler` — zero errors.
|
||||
- [ ] `bunx turbo test --filter=@suaveplan/profiler` — all tests pass.
|
||||
- [ ] `bunx turbo test:coverage --filter=@suaveplan/profiler` — 100% on all touched modules.
|
||||
- [ ] `bun run docs:check --filter=@suaveplan/profiler` — zero errors.
|
||||
- [ ] `bun scripts/publish.ts --package @suaveplan/profiler`.
|
||||
- [ ] Update `CLAUDE.md` package listing.
|
||||
- [ ] Archive: move `openspec/changes/enterprise-profiler/` to `openspec/archive/2026-04-24-enterprise-profiler/`.
|
||||
Reference in New Issue
Block a user