6.5 KiB
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
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
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
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
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
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
type ProfilerErrorCode =
| 'PROFILER_SESSION_NOT_FOUND'
| 'PROFILER_SESSION_ALREADY_STOPPED'
| 'PROFILER_EXPORT_FAILED'
| 'PROFILER_SAMPLE_FAILED'
| 'PROFILER_SINK_FAILED'
function registerProfilerErrorCodes(): void
Genesis additions
- Structured
ProfilerErrorextendingSuaveplanErrorwith stablePROFILER.*codes - Injectable
Clockso sample timestamps are deterministic in tests ProfilerSinkplugin interface —use()onProfiler; console and JSON sinks built-inAbortSignalonsession.export()- OTel integration — span
profiler.sessionwith attributessession.name,session.sample_count; histogramprofiler.session_duration_ms aggregateSampleswith percentile stats (p50/p95/p99) computed via insertion sort on small sample sets- Property-based tests for
aggregateSamplesstatistical invariants,buildFlamegraphtree structure - Benchmark suite —
sample()hot path,buildFlamegraphon 1k/10k sample sessions
File layout
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.