Files
2026-05-27 23:29:53 +01:00

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

  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

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.