Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
# @suaveplan/profiler - Universal runtime performance profiler — Profiler/Sampler/MemoryProfiler/BottleneckDetector/ProfileComparator/FlameGraphGenerator/ProfileReporter/OtelExporter with injectable Clock+TimersLike+HeapCapture, PROFILER_CODES error registry, and OTel self-telemetry.
Universal runtime performance profiler.
<!-- begin:auto-docs -->
## Documentation tree
- [bottleneck](./src/bottleneck/bottleneck.md) — `BottleneckDetector` analyses arrays of `ProfileEntry` records to surface performance bottlenecks. It groups entries by…
- [bottleneck](./src/bottleneck/bottleneck.md) — `BottleneckDetector` analyses arrays of `ProfileEntry` records to surface performance bottlenecks. It groups entries by…
- [comparison](./src/comparison/comparison.md) — `ProfileComparator` diffs two sets of profiling data — either raw `ProfileEntry[]` arrays or pre-aggregated `ProfileRep…
- [comparison](./src/comparison/comparison.md) — `ProfileComparator` diffs two sets of profiling data — either raw `ProfileEntry[]` arrays or pre-aggregated `ProfileRep…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable error-code registry for `@suaveplan/profiler` and the typed `ProfilerError` class consumers throw fr…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable error-code registry for `@suaveplan/profiler` and the typed `ProfilerError` class consumers throw fr…
- [flamegraph](./src/flamegraph/flamegraph.md) — `FlameGraphGenerator` builds a flame-graph data structure from `ProfileEntry` arrays by nesting entries on start/end-ti…
- [flamegraph](./src/flamegraph/flamegraph.md) — `FlameGraphGenerator` builds a flame-graph data structure from `ProfileEntry` arrays by nesting entries on start/end-ti…
- [memory](./src/memory/memory.md) — `MemoryProfiler` captures periodic heap snapshots, records labelled allocation totals, tracks a peak-heap watermark, an…
- [memory](./src/memory/memory.md) — `MemoryProfiler` captures periodic heap snapshots, records labelled allocation totals, tracks a peak-heap watermark, an…
- [otel](./src/otel/otel.md) — `OtelExporter` converts `ProfileEntry` records to OTLP-compatible spans and optionally exports them to a collector endp…
- [otel](./src/otel/otel.md) — `OtelExporter` converts `ProfileEntry` records to OTLP-compatible spans and optionally exports them to a collector endp…
- [profiler](./src/profiler/profiler.md) — `Profiler` is the core measurement primitive of `@suaveplan/profiler`. It records timing entries between paired `start(…
- [profiler](./src/profiler/profiler.md) — `Profiler` is the core measurement primitive of `@suaveplan/profiler`. It records timing entries between paired `start(…
- [reporter](./src/reporter/reporter.md) — `ProfileReporter` computes summary statistics from profiling entries — count, min, max, mean, median, configurable perc…
- [reporter](./src/reporter/reporter.md) — `ProfileReporter` computes summary statistics from profiling entries — count, min, max, mean, median, configurable perc…
- [sampler](./src/sampler/sampler.md) — `Sampler` collects periodic snapshots of runtime metrics (memory usage by default, plus caller-supplied custom metrics)…
- [sampler](./src/sampler/sampler.md) — `Sampler` collects periodic snapshots of runtime metrics (memory usage by default, plus caller-supplied custom metrics)…
- [telemetry](./src/telemetry/telemetry.md) — Owns the OpenTelemetry meter, counters, and histograms emitted by `@suaveplan/profiler` itself. Every public completion…
- [telemetry](./src/telemetry/telemetry.md) — Owns the OpenTelemetry meter, counters, and histograms emitted by `@suaveplan/profiler` itself. Every public completion…
<!-- end:auto-docs -->
## Modules
- [`profiler/`](./src/profiler/profiler.md) — `Profiler`: start/stop/mark/measure with injectable `Clock`.
- [`sampler/`](./src/sampler/sampler.md) — `Sampler`: periodic metric collection driven by `TimersLike`.
- [`memory/`](./src/memory/memory.md) — `MemoryProfiler`: heap snapshots, allocation tracking, leak detection.
- [`bottleneck/`](./src/bottleneck/bottleneck.md) — `BottleneckDetector`: hot-spot + budget-violation analysis.
- [`comparison/`](./src/comparison/comparison.md) — `ProfileComparator`: regressions / improvements / unchanged.
- [`flamegraph/`](./src/flamegraph/flamegraph.md) — `FlameGraphGenerator`: containment-tree builder + d3 JSON.
- [`reporter/`](./src/reporter/reporter.md) — `ProfileReporter`: percentile stats + text format.
- [`otel/`](./src/otel/otel.md) — `OtelExporter`: OTLP-compatible JSON exporter.
- [`error-codes/`](./src/error-codes/error-codes.md) — `PROFILER_CODES` + `ProfilerError`.
- [`telemetry/`](./src/telemetry/telemetry.md) — OTel meter for the profiler itself.
## Install
```bash
bun add @suaveplan/profiler
```
## Usage
```ts
import { Profiler, ProfileReporter, OtelExporter } from "@suaveplan/profiler";
const profiler = new Profiler();
profiler.start("render", "ui");
// ... work ...
profiler.stop("render");
const reporter = new ProfileReporter({ alertThreshold: 16 });
console.log(reporter.formatReport(reporter.generateReport(profiler.getEntries())));
const exporter = new OtelExporter({ serviceName: "my-app" });
await exporter.export(exporter.convertEntries(profiler.getEntries()));
```
## License
MIT

View File

@@ -0,0 +1,59 @@
# bottleneck
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`BottleneckDetector` analyses arrays of `ProfileEntry` records to surface performance bottlenecks. It groups entries by label, computes per-group average duration and frequency, and ranks results by impact (`avgDuration * frequency`). Detection is pure and stateless — the same detector can be applied to entries from any `Profiler` instance without coupling.
## Features
- Zod-validated configuration (`timeBudgetMs > 0`, `hotSpotFrequency > 0`, `maxResults > 0`).
- `analyze(entries)` — returns up to `maxResults` bottlenecks with attached `suggestion` strings, ranked by impact descending.
- `findBudgetViolations(entries)` — returns every entry whose `duration` exceeds `timeBudgetMs`.
- `findHotSpots(entries)` — returns entries that are BOTH slower than budget AND occur at least `hotSpotFrequency` times.
- Suggestion tiering: hot-spot, massively-over-budget (>10x), over-budget, and very-frequent thresholds each produce a distinct human-readable hint.
## Usage: Basic
```ts
import { BottleneckDetector } from "@suaveplan/profiler";
const detector = new BottleneckDetector({ timeBudgetMs: 16 });
const bottlenecks = detector.analyze(profiler.getEntries());
for (const b of bottlenecks) {
console.warn(b.suggestion);
}
```
## Usage: Advanced
```ts
import { BottleneckDetector } from "@suaveplan/profiler";
const detector = new BottleneckDetector({
timeBudgetMs: 4, // sub-frame budget for 240Hz displays
hotSpotFrequency: 50,
maxResults: 5,
});
const violations = detector.findBudgetViolations(profiler.getEntries());
const hotSpots = detector.findHotSpots(profiler.getEntries());
if (hotSpots.length > 0) {
metrics.bottlenecks.add(hotSpots.length, { tier: "hot-spot" });
}
```
## API Reference
- `class BottleneckDetector` — `new BottleneckDetector(config?: BottleneckConfig)`.
- `interface BottleneckConfig { timeBudgetMs?, hotSpotFrequency?, maxResults? }`.
- Methods: `analyze`, `findBudgetViolations`, `findHotSpots`.
- Errors: `ProfilerError` with `PROFILER.INVALID_OPTIONS` on bad config.
## Implementation Notes
`analyze` filters out entries with `duration === undefined` (still-running measurements), groups by label, and dispatches to `generateSuggestion`. The suggestion tiers are checked in priority order so a hot-spot label takes precedence over the massive-over-budget tier even when both apply, and the very-frequent tier only fires when an entry is cheap-but-noisy. Sort is stable and deterministic — equal impacts retain insertion order, which keeps the output reproducible across runs.

View File

@@ -0,0 +1,57 @@
# comparison
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ProfileComparator` diffs two sets of profiling data — either raw `ProfileEntry[]` arrays or pre-aggregated `ProfileReport` objects — and classifies each per-label delta as a regression, an improvement, or unchanged. The classification threshold and the wall-clock used for the report's `generatedAt` field are both injectable, so test runs are deterministic and CI gates can configure their own sensitivity.
## Features
- `compareReports(baseline, current)` operates at category granularity (uses `report.byCategory` means).
- `compareEntries(baseline, current)` operates at label granularity (computes per-label averages internally).
- `regressionThreshold` controls how many percent of change is required to classify a delta as a regression or improvement. Default 5.
- Sorts: regressions descending by `percentChange`, improvements ascending.
- Zero-baseline edge case handled symmetrically — a label that appears only in `current` is classified by treating baseline as 0 and reporting 100% change.
## Usage: Basic
```ts
import { ProfileComparator, ProfileReporter } from "@suaveplan/profiler";
const reporter = new ProfileReporter();
const baseline = reporter.generateReport(oldEntries);
const current = reporter.generateReport(newEntries);
const diff = new ProfileComparator().compareReports(baseline, current);
console.log("regressions:", diff.regressions.length);
```
## Usage: Advanced
```ts
import { ProfileComparator } from "@suaveplan/profiler";
const comparator = new ProfileComparator({
regressionThreshold: 2, // tighter than default 5%
clock: () => Date.parse("2025-01-01T00:00:00Z"),
});
const diff = comparator.compareEntries(baseline.getEntries(), pr.getEntries());
if (diff.regressions.length > 0) {
process.exitCode = 1;
console.error("perf regression gate triggered:", diff.regressions);
}
```
## API Reference
- `class ProfileComparator` — `new ProfileComparator(options?: ProfileComparatorOptions)`.
- `interface ProfileComparatorOptions { regressionThreshold?: number; clock?: Clock }`.
- Methods: `compareReports(baseline, current)`, `compareEntries(baseline, current)`.
- Errors: `ProfilerError` with `PROFILER.INVALID_OPTIONS` on negative threshold.
## Implementation Notes
`compareEntries` ignores entries with `duration === undefined` so partially-completed profiles can be compared safely. Per-label averages are computed from raw durations rather than from any pre-aggregated stats so the same comparator can mix labels with different occurrence counts in baseline vs current. The `unchanged` bucket exists explicitly so consumers wanting a full per-label audit can read it without re-grouping. The `generatedAt` timestamp is produced from `new Date(clock()).toISOString()` so the default and the injected clock paths produce identically-formatted strings.

View File

@@ -0,0 +1,59 @@
# error-codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable error-code registry for `@suaveplan/profiler` and the typed `ProfilerError` class consumers throw from every failure path. Codes are registered with `@suaveplan/error`'s `defineCodes` helper so they survive serialisation and module-boundary crossings — consumers discriminate on `error.code` rather than `instanceof`, which keeps the contract portable across worker threads, HTTP wire formats, and language-server log scrapes.
## Features
- Seven canonical codes: `INVALID_OPTIONS`, `UNKNOWN_LABEL`, `SAMPLER_ALREADY_RUNNING`, `SAMPLER_NOT_RUNNING`, `MEMORY_ALREADY_RUNNING`, `MEMORY_NOT_RUNNING`, `EXPORT_FAILED`.
- Every code namespaced under the `PROFILER.` prefix at registration time.
- `ProfilerError` extends `SuaveplanError<ProfilerCode>` so the `code` field narrows against the literal union and `switch (err.code)` is exhaustively type-checked.
- Public constructor: `SuaveplanError`'s own constructor is protected, so we delegate via `super(code, message, options)` and set `this.name = "ProfilerError"` so stack frames stay readable.
- `ProfilerError` accepts `{ cause?, context? }` — `cause` chains underlying errors, `context` ships structured details to log sinks.
## Usage: Basic
```ts
import { PROFILER_CODES, ProfilerError } from "@suaveplan/profiler";
throw new ProfilerError(
PROFILER_CODES.INVALID_OPTIONS,
"Sampler intervalMs must be positive"
);
```
## Usage: Advanced
```ts
try {
new Sampler({ intervalMs: -1 });
} catch (err) {
if (err instanceof ProfilerError) {
switch (err.code) {
case PROFILER_CODES.INVALID_OPTIONS:
logger.warn("config rejected", err.context);
break;
case PROFILER_CODES.EXPORT_FAILED:
metrics.exportFailures.add(1);
break;
default:
throw err;
}
}
}
```
## API Reference
- `PROFILER_CODES: Readonly<{ INVALID_OPTIONS: "PROFILER.INVALID_OPTIONS", ... }>` — frozen literal-type record.
- `type ProfilerCode = (typeof PROFILER_CODES)[keyof typeof PROFILER_CODES]` — discriminated union.
- `class ProfilerError extends SuaveplanError<ProfilerCode>` — public `constructor(code, message, options?)`.
## Implementation Notes
The registry is built with `defineCodes("PROFILER", [...] as const)` so TypeScript can infer the keys as literal types. Adding a new code requires appending to the `as const` array and to the OpenSpec spec delta — no other change is needed because the union and registry update together. The `ProfilerError` constructor is intentionally minimal: it delegates to `SuaveplanError`, sets `this.name`, and accepts the same options bundle so error logs and OTel span events render correctly out of the box.

View File

@@ -0,0 +1,54 @@
# flamegraph
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`FlameGraphGenerator` builds a flame-graph data structure from `ProfileEntry` arrays by nesting entries on start/end-time containment. The output is a `FlameNode` tree compatible with `d3-flame-graph` and similar visualisers; SVG/HTML rendering is intentionally out of scope (a downstream concern), but `toD3Format` produces the exact JSON shape these libraries expect.
## Features
- Pure containment algorithm: an entry whose `[startTime, endTime]` interval is fully inside another's becomes a child of it.
- Tie-breaking on equal start times: wider spans first so they own the narrower children.
- Self-time computation: `selfTime = value - sum(children.value)`, never negative.
- `collapseRecursive(node)` merges consecutive same-name parent/child pairs, useful for cleaning up tail-recursive call patterns before rendering.
- `toD3Format(node)` strips internal fields (`selfTime`, `startTime`, `endTime`) so the JSON is exactly `{ name, value, children }`.
## Usage: Basic
```ts
import { FlameGraphGenerator } from "@suaveplan/profiler";
const gen = new FlameGraphGenerator();
const tree = gen.generate(profiler.getEntries());
const d3Json = gen.toD3Format(tree);
```
## Usage: Advanced
```ts
import { FlameGraphGenerator } from "@suaveplan/profiler";
const gen = new FlameGraphGenerator();
const rawTree = gen.generate(entries);
const collapsed = gen.collapseRecursive(rawTree);
// rawTree shows the full recursion stack; collapsed flattens
// `parser → parser → parser → leaf` into `parser → leaf`.
const json = gen.toD3Format(collapsed);
await writeFile("flame.json", JSON.stringify(json));
```
## API Reference
- `class FlameGraphGenerator` — no constructor args (stateless).
- Methods:
- `generate(entries: readonly ProfileEntry[]): FlameNode`
- `toD3Format(node: FlameNode): { name, value, children }`
- `collapseRecursive(node: FlameNode): FlameNode`
## Implementation Notes
The internal `MutableFlameNode` carries `startTime`/`endTime` for the duration of tree construction so containment checks remain O(1) per node-pair; the final `freeze` step drops these fields and returns the immutable `FlameNode` shape. When a new entry is inserted, the algorithm first checks for a containing existing child (recursing into it) and otherwise creates a new node, absorbing any existing siblings the new entry contains. This handles arbitrary insertion order — the input is sorted by start-time ascending, ties broken by wider span first, before insertion starts.

View File

@@ -0,0 +1,66 @@
# memory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MemoryProfiler` captures periodic heap snapshots, records labelled allocation totals, tracks a peak-heap watermark, and runs a simple trend-based leak detector. Heap readings come from an injectable `HeapCapture` so test runs are deterministic and non-Node environments can plug their own probe. Timer source is `TimersLike`, matching `Sampler`.
## Features
- Zod-validated config (`sampleIntervalMs > 0`, `maxSnapshots > 0`, `leakThresholdBytes ≥ 0`, `leakWindowSize ≥ 2`).
- Injectable `Clock`, `TimersLike`, `HeapCapture`. Defaults read `process.memoryUsage` then `performance.memory` then return zeros.
- `start()` / `stop()` are idempotent — double-start and double-stop are no-ops.
- `takeSnapshot(label?)` records a snapshot; FIFO-evicts beyond `maxSnapshots`.
- `recordAllocation(label, bytes)` aggregates `allocations` / `totalBytes` / `peakBytes` per label.
- `detectLeak()` returns true when the last `leakWindowSize` snapshots are monotonically non-decreasing AND total growth meets `leakThresholdBytes`.
- `triggerGC()` invokes `globalThis.gc()` when Node was started with `--expose-gc`.
- `events: EventBus<MemoryProfilerEventMap>` — `memory:start`, `memory:stop`, `memory:snapshot`, `memory:leak-detected`.
- Telemetry: `profiler_memory_snapshots_total` counter on every snapshot.
## Usage: Basic
```ts
import { MemoryProfiler } from "@suaveplan/profiler";
const mp = new MemoryProfiler({ sampleIntervalMs: 1000 });
mp.start();
// ...
mp.stop();
if (mp.detectLeak()) console.warn("leak suspected");
```
## Usage: Advanced
```ts
import { MemoryProfiler } from "@suaveplan/profiler";
const fakeHeap: Array<{ heapUsed: number; heapTotal: number }> = [
{ heapUsed: 1_000_000, heapTotal: 10_000_000 },
{ heapUsed: 2_000_000, heapTotal: 10_000_000 },
{ heapUsed: 3_000_000, heapTotal: 10_000_000 },
];
let i = 0;
const mp = new MemoryProfiler({
leakWindowSize: 3,
leakThresholdBytes: 1_500_000,
heapCapture: () => fakeHeap[i++] ?? fakeHeap[fakeHeap.length - 1]!,
});
mp.events.on("memory:leak-detected", ({ snapshots }) => {
log("leak window", snapshots);
});
for (let n = 0; n < 3; n++) mp.takeSnapshot();
mp.detectLeak(); // true → emits memory:leak-detected
```
## API Reference
- `class MemoryProfiler` — `new MemoryProfiler(options?: MemoryProfilerOptions)`.
- `interface MemoryProfilerOptions extends MemoryProfilerConfig { clock?, timers?, heapCapture? }`.
- Methods: `start`, `stop`, `takeSnapshot`, `recordAllocation`, `getAllocations`, `getAllocation`, `getSnapshots`, `getPeakHeapUsed`, `isRunning`, `detectLeak`, `triggerGC`, `clear`.
## Implementation Notes
The leak detector walks the last `leakWindowSize` snapshots and counts strictly-increasing transitions; a leak is declared only when EVERY transition is non-decreasing AND end-minus-start exceeds the threshold. That eliminates the false-positive case where one large jump in an otherwise flat window would otherwise read as a leak. `triggerGC` is a best-effort hook — Node only exposes `globalThis.gc` under `--expose-gc`, so the false branch matters in production.

View File

@@ -0,0 +1,63 @@
# otel
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`OtelExporter` converts `ProfileEntry` records to OTLP-compatible spans and optionally exports them to a collector endpoint over HTTP. Both the `fetch` implementation and the random-bytes producer used for trace/span IDs are injectable so test runs can assert exact payloads and produce deterministic IDs.
## Features
- Zod-validated config (`endpoint` MUST be a valid URL when supplied; `serviceName` non-empty; `batchSize > 0`).
- Defaults: `http://localhost:4318/v1/traces`, `serviceName: "profiler"`, `batchSize: 100`.
- `entryToSpan(entry, traceId?, parentSpanId?)` — single conversion; defaults trace/span IDs from the injected RNG.
- `convertEntries(entries, traceId?)` — batch conversion under a single shared trace.
- `addToBatch(spans)` / `flush()` / `getPendingCount()` — pending-batch buffer.
- `export(spans)` — POSTs an OTLP JSON payload; returns `true` on `response.ok`, `false` on non-ok responses or thrown errors (no exceptions escape).
- `exportBatch()` — drains pending in `batchSize` slices, returning the count of successfully exported spans.
- Attribute encoding: strings → `stringValue`, integers → `intValue`, non-integer numbers → `doubleValue`, booleans → `boolValue`.
## Usage: Basic
```ts
import { OtelExporter } from "@suaveplan/profiler";
const exporter = new OtelExporter({ serviceName: "my-app" });
const spans = exporter.convertEntries(profiler.getEntries());
await exporter.export(spans);
```
## Usage: Advanced
```ts
import { OtelExporter } from "@suaveplan/profiler";
const seed = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
let cursor = 0;
const exporter = new OtelExporter({
endpoint: "https://collector.internal/v1/traces",
headers: { "x-api-key": process.env.OTLP_KEY ?? "" },
batchSize: 25,
randomBytes: (n) => seed.slice(cursor, (cursor += n)),
fetchFn: async (url, init) => {
const res = await myInstrumentedFetch(url, init);
return { ok: res.ok };
},
});
exporter.addToBatch(exporter.convertEntries(profiler.getEntries()));
const ok = await exporter.exportBatch();
```
## API Reference
- `class OtelExporter` — `new OtelExporter(config?: OtelExporterConfig)`.
- `interface OtelExporterConfig { endpoint?, serviceName?, batchSize?, headers?, fetchFn?, randomBytes? }`.
- Methods: `generateTraceId`, `generateSpanId`, `entryToSpan`, `convertEntries`, `addToBatch`, `getPendingCount`, `flush`, `export`, `exportBatch`.
- Errors: `ProfilerError` with `PROFILER.INVALID_OPTIONS` on bad config.
## Implementation Notes
The payload structure follows the OTLP/HTTP JSON encoding: a single `resourceSpans` entry with one `scopeSpans` group, and per-span attributes encoded as `{ key, value }` records where `value` is a discriminated union by primitive type. `metadata` fields that are objects/arrays are silently dropped — OTLP only allows primitive attribute values, and reshaping nested metadata is the caller's responsibility. Export failures are swallowed and surfaced via the boolean return so exporters can be used inside fire-and-forget profiling loops without crashing the host.

View File

@@ -0,0 +1,64 @@
# profiler
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`Profiler` is the core measurement primitive of `@suaveplan/profiler`. It records timing entries between paired `start(label)` / `stop(label)` calls, places named marks on a shared timeline, and offers `measure` / `measureAsync` helpers that wrap sync/async functions and stop the entry even when the wrapped function throws. The wall-clock source is injected through a `Clock` so tests can drive time deterministically and so non-Node environments without `performance.now()` can plug in their own reader.
## Features
- `start(label, category?, metadata?)` — registers an active entry keyed by `label`.
- `stop(label)` — completes the entry, computes `duration`, emits `profile:stop` on the bus, increments `profiler_entries_total`, and records the duration on the histogram.
- `mark(label, metadata?)` — places a single timestamped mark, increments `profiler_marks_total`.
- `measure(label, fn, category?)` — sync wrapper; uses `try/finally` so `stop` runs on throw.
- `measureAsync(label, fn, category?)` — async wrapper with the same `try/finally` discipline.
- `isActive(label)` / `getEntries()` / `getMarks()` / `getEntriesByCategory(cat)` / `clear()` — introspection + reset.
- `events: EventBus<ProfilerEventMap>` — single-payload events compatible with `@suaveplan/events`.
## Usage: Basic
```ts
import { Profiler } from "@suaveplan/profiler";
const profiler = new Profiler();
profiler.start("render", "ui");
// ... work ...
const entry = profiler.stop("render");
console.log(entry?.duration);
```
## Usage: Advanced
```ts
import { Profiler } from "@suaveplan/profiler";
let t = 0;
const profiler = new Profiler({ clock: () => t });
t = 0;
const result = await profiler.measureAsync(
"io",
async () => {
t = 50; // simulate elapsed time
return await loadConfig();
},
"network"
);
const entries = profiler.getEntriesByCategory("network");
// entries[0].duration === 50
```
## API Reference
- `class Profiler` — public constructor `new Profiler(options?: ProfilerOptions)`.
- `interface ProfilerOptions { clock?: Clock }` — `Clock` is `() => number` from `@suaveplan/types`.
- Methods: `start`, `stop`, `mark`, `measure`, `measureAsync`, `getEntries`, `getMarks`, `getEntriesByCategory`, `isActive`, `clear`.
- `events` — `EventBus<ProfilerEventMap>` exposing `profile:start`, `profile:stop`, `profile:mark`, `profile:clear`.
## Implementation Notes
The default clock prefers `performance.now()` (high-resolution monotonic) and falls back to `Date.now()` when `performance` is unavailable. Active entries are stored in a `Map<string, …>` keyed by label, so calling `start` twice with the same label overwrites the start timestamp — that matches the convention of `performance.mark`/`performance.measure`. `measure` and `measureAsync` use `try { return fn(); } finally { stop(label); }` so the entry is always closed even on throw, which is critical for accurate `getEntries()` data when faults are common.

View File

@@ -0,0 +1,57 @@
# reporter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ProfileReporter` computes summary statistics from profiling entries — count, min, max, mean, median, configurable percentiles, total duration, per-category groupings, and an alerts list for entries above a threshold. It also offers `formatReport(report)` to produce a stable human-readable text rendering suitable for logs and CI annotations.
## Features
- Zod-validated config: `percentiles` MUST be a non-empty array of numbers in `[0, 100]`; `alertThreshold` ≥ 0.
- Configurable percentile list (default `[50, 90, 95, 99]`).
- Linear-interpolation percentile estimator — matches what most statistics libraries call "type 7" / R's default.
- Per-category groupings keyed by `entry.category` (`uncategorized` when absent).
- Alerts: entries whose `duration` strictly exceeds `alertThreshold`.
- Injectable `Clock` so the report's `generatedAt` timestamp is deterministic in tests.
## Usage: Basic
```ts
import { ProfileReporter } from "@suaveplan/profiler";
const reporter = new ProfileReporter();
const out = reporter.generateReport(profiler.getEntries(), profiler.getMarks());
console.log(reporter.formatReport(out));
```
## Usage: Advanced
```ts
import { ProfileReporter } from "@suaveplan/profiler";
const reporter = new ProfileReporter({
percentiles: [75, 90, 95, 99, 99.9],
alertThreshold: 16,
clock: () => Date.parse("2025-01-01T00:00:00Z"),
});
const out = reporter.generateReport(entries);
const renderStats = out.byCategory.render;
if ((renderStats?.percentiles[99] ?? 0) > 16) {
metrics.frameMisses.add(1);
}
```
## API Reference
- `class ProfileReporter` — `new ProfileReporter(options?: ProfileReporterOptions)`.
- `interface ProfileReporterOptions extends ReporterConfig { clock?: Clock }`.
- Methods: `generateReport(entries, marks?)`, `computeStats(durations)`, `formatReport(report)`.
- Errors: `ProfilerError` with `PROFILER.INVALID_OPTIONS` on bad config.
## Implementation Notes
`computeStats` sorts a copy of the input durations and reads min/max from the endpoints. The percentile estimator uses the formula `index = (p/100) * (n-1)`, with linear interpolation between neighbouring sorted values when the index is fractional. Empty input returns a fully-zeroed `ProfileStats` so callers can treat the result uniformly. `formatReport` omits the Alerts section when none fire to keep typical log output compact.

View File

@@ -0,0 +1,71 @@
# sampler
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`Sampler` collects periodic snapshots of runtime metrics (memory usage by default, plus caller-supplied custom metrics). The timer source is injected through a `TimersLike` so tests can advance virtual time deterministically and so non-`setInterval` runtimes (Workers, Deno, embedded JS) can plug in their own scheduler. Sample storage is capped via FIFO eviction so long-running collectors don't leak memory.
## Features
- Zod-validated options (`intervalMs > 0`, optional positive `maxSamples`, optional `captureMemory`).
- Injectable `Clock`, `TimersLike`, and `MemoryProbe` — the defaults read `Date.now`, `setTimeout`/`clearTimeout`, and `process.memoryUsage`/`performance.memory` respectively.
- `start()` / `stop()` are idempotent — double-`start` and double-`stop` are no-ops.
- `takeSample(customMetrics?)` records a single sample on demand; same code path used by the scheduled tick.
- `getSamples()` / `getSamplesInRange(start, end)` / `clear()` for retrieval and reset.
- `events: EventBus<SamplerEventMap>` emits `sampler:start`, `sampler:stop`, `sampler:sample`.
- Telemetry: `profiler_samples_total` counter incremented on every sample.
## Usage: Basic
```ts
import { Sampler } from "@suaveplan/profiler";
const sampler = new Sampler({ intervalMs: 1000 });
sampler.start();
// ...
sampler.stop();
console.log(sampler.getSamples());
```
## Usage: Advanced
```ts
import { Sampler } from "@suaveplan/profiler";
import type { TimersLike } from "@suaveplan/types";
const queue: Array<{ cb: () => void; handle: number }> = [];
let next = 1;
const fakeTimers: TimersLike = {
setTimer(cb) {
const handle = next++;
queue.push({ cb, handle });
return handle;
},
clearTimer() {},
};
let clockT = 0;
const sampler = new Sampler({
intervalMs: 10,
timers: fakeTimers,
clock: () => clockT,
memoryProbe: () => 4096,
});
sampler.start();
clockT = 10;
queue.shift()?.cb();
```
## API Reference
- `class Sampler` — `new Sampler(options: SamplerOptions)`.
- `interface SamplerOptions extends SamplerConfig { clock?, timers?, memoryProbe? }`.
- Methods: `start`, `stop`, `takeSample`, `getSamples`, `getSamplesInRange`, `isRunning`, `clear`.
- Errors: throws `ProfilerError` with `PROFILER.INVALID_OPTIONS` when options fail Zod validation.
## Implementation Notes
The scheduler uses a self-rescheduling `setTimer` rather than `setInterval` because `TimersLike` only models one-shot timers. Each tick checks `this.running` before taking a sample, which lets a stop() that runs between callback dispatch and execution avoid emitting a stale sample. FIFO eviction uses `splice(0, n)` so the oldest samples drop out when `maxSamples` is exceeded.

View File

@@ -0,0 +1,59 @@
# telemetry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the OpenTelemetry meter, counters, and histograms emitted by `@suaveplan/profiler` itself. Every public completion path inside `Profiler`, `Sampler`, and `MemoryProfiler` routes through the `recordX` helpers defined here so consumers can observe the profiler the same way they observe any other instrumented service — counters tag entries by category when present, and the histogram captures the measurement-duration distribution.
## Features
- Lazy-bound meter via `metrics.getMeter("@suaveplan/profiler")` — resolves to a no-op meter when no `MeterProvider` is registered, so importing the module is free in production code paths that opt out of telemetry.
- Five instruments:
- `profiler_entries_total` (counter, tagged by `category` when present)
- `profiler_marks_total` (counter)
- `profiler_samples_total` (counter)
- `profiler_memory_snapshots_total` (counter)
- `profiler_measure_duration_ms` (histogram, unit `ms`)
- Pure record helpers (`recordEntry` / `recordMark` / `recordMeasureDuration` / `recordSample` / `recordMemorySnapshot`) so call sites stay readable.
## Usage: Basic
```ts
import { recordEntry } from "@suaveplan/profiler";
recordEntry("render");
```
## Usage: Advanced
Wire a real `MeterProvider` in tests using `@opentelemetry/sdk-metrics`. The package ships a `tests/setup/polyfills.ts` that registers an `InMemoryMetricExporter` BEFORE the source modules import, so the lazy meter resolves to the test provider:
```ts
import { metrics } from "@opentelemetry/api";
import {
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
AggregationTemporality,
} from "@opentelemetry/sdk-metrics";
const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
const reader = new PeriodicExportingMetricReader({ exporter });
metrics.setGlobalMeterProvider(new MeterProvider({ readers: [reader] }));
```
## API Reference
- `entriesCounter: Counter`, `marksCounter: Counter`, `samplesCounter: Counter`, `memorySnapshotsCounter: Counter`, `measureDurationHistogram: Histogram` — direct instrument access.
- `recordEntry(category: string | undefined): void`
- `recordMark(): void`
- `recordMeasureDuration(durationMs: number): void`
- `recordSample(): void`
- `recordMemorySnapshot(): void`
## Implementation Notes
The meter is acquired at module load (`metrics.getMeter(METER_NAME)`), which means a test runner that wants real metric points must install its `MeterProvider` via `setupFiles` before the SDK imports. The provided `polyfills.ts` does exactly that. Counters are added with `+1` per event so totals are linear; the histogram is recorded with the measured duration in milliseconds so percentile aggregations work out of the box.

View File

@@ -0,0 +1,70 @@
# profiler.types
## Purpose
Shared, value-free type definitions consumed by every feature module of `@suaveplan/profiler`. Centralising the record shapes here keeps cross-module imports cheap (types vanish at build time) and gives downstream consumers a single import surface for the public contract.
## Features
- `ProfileEntry`, `ProfileMark`, `SamplePoint` — the three atomic measurement records.
- `ProfileStats`, `ProfileReport` — output shapes for the reporter.
- `MemorySnapshot`, `AllocationRecord`, `MemoryProfilerConfig`, `HeapCapture`, `MemoryProbe` — memory subsystem records.
- `FlameNode`, `Bottleneck`, `BottleneckConfig`, `ComparisonReport`, `ProfileDelta` — analysis record shapes.
- `OtelSpan`, `OtelExporterConfig`, `FetchLike`, `RandomBytes` — OTLP exporter contract.
- `ProfilerEventMap`, `SamplerEventMap`, `MemoryProfilerEventMap` — single-payload event maps compatible with `@suaveplan/events`'s `EventBus<TMap>` constraint.
- All optional fields are explicitly typed as `T | undefined` so the package compiles cleanly under `exactOptionalPropertyTypes: true`.
## Usage: Basic
```ts
import type { ProfileEntry, ProfilerEventMap } from "@suaveplan/profiler";
const entry: ProfileEntry = {
label: "render",
startTime: 0,
endTime: 16,
duration: 16,
category: "ui",
};
```
## Usage: Advanced
```ts
import { EventBus } from "@suaveplan/events";
import type { ProfilerEventMap, ProfileEntry } from "@suaveplan/profiler";
const bus = new EventBus<ProfilerEventMap>();
bus.on("profile:stop", ({ entry }: { entry: ProfileEntry }) => {
console.log(entry.label, entry.duration);
});
```
## API Reference
- `interface ProfileEntry { label, startTime, endTime?, duration?, category?, metadata? }`
- `interface ProfileMark { label, timestamp, metadata? }`
- `interface SamplePoint { timestamp, memoryUsage?, metrics? }`
- `interface SamplerConfig { intervalMs, captureMemory?, maxSamples? }`
- `interface ReporterConfig { percentiles?, alertThreshold? }`
- `interface ProfileStats { count, min, max, mean, median, percentiles, totalDuration }`
- `interface ProfileReport { generatedAt, stats, byCategory, alerts, marks }`
- `interface MemorySnapshot { timestamp, heapUsed, heapTotal, label? }`
- `interface AllocationRecord { label, allocations, totalBytes, peakBytes }`
- `interface MemoryProfilerConfig { sampleIntervalMs?, maxSnapshots?, leakThresholdBytes?, leakWindowSize? }`
- `type HeapCapture = () => { heapUsed: number; heapTotal: number }`
- `type MemoryProbe = () => number | undefined`
- `interface FlameNode { name, value, selfTime, children }`
- `interface BottleneckConfig { timeBudgetMs?, hotSpotFrequency?, maxResults? }`
- `interface Bottleneck { label, category?, avgDuration, frequency, impact, suggestion }`
- `interface OtelSpan { traceId, spanId, parentSpanId?, name, startTimeUnixNano, endTimeUnixNano, attributes }`
- `type RandomBytes = (n: number) => Uint8Array`
- `type FetchLike = (url, init) => Promise<{ ok: boolean }>`
- `interface OtelExporterConfig { endpoint?, serviceName?, batchSize?, headers?, fetchFn?, randomBytes? }`
- `interface ProfileDelta { label, category?, baselineDuration, currentDuration, absoluteChange, percentChange }`
- `interface ComparisonReport { generatedAt, regressions, improvements, unchanged, overallDelta, overallPercentChange }`
- `type ProfilerEventMap`, `type SamplerEventMap`, `type MemoryProfilerEventMap` — single-payload event maps.
## Implementation Notes
Event maps are declared as `type` aliases rather than `interface`s because TypeScript treats an `interface` as not automatically assignable to `Record<string, unknown>` — the constraint `@suaveplan/events`'s `EventBus<TMap extends EventMap>` enforces — while `type` aliases over object literals are. Optional `T | undefined` annotations are deliberate: under `exactOptionalPropertyTypes: true` the form `prop?: T` is strictly distinct from `prop?: T | undefined`, and the codebase frequently assigns `undefined` explicitly when building records programmatically.