# 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 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 } interface FlamegraphNode { readonly name: string readonly value: number readonly children: ReadonlyArray } ``` ### Profiler API ```ts function createProfiler(options?: ProfilerOptions): Profiler interface Profiler { startSession(name: string, options?: SessionOptions): ProfilerSession getSession(id: string): ProfilerSession | null listSessions(): ReadonlyArray 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 toFlamegraph(): FlamegraphNode } ``` ### Options ```ts interface ProfilerOptions { sampleIntervalMs?: number maxSamples?: number collectMemory?: boolean collectCpu?: boolean clock?: Clock logger?: LoggerLike eventBus?: EventBusLike } interface SessionOptions { tags?: Record autoStop?: boolean autoStopAfterMs?: number } ``` ### Plugin/sink interface ```ts interface ProfilerSink { readonly name: string onSessionEnd(session: ProfileSession): void | Promise } 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.