first commit
This commit is contained in:
106
docs/networking/network/network-plugin-devtools/README.md
Normal file
106
docs/networking/network/network-plugin-devtools/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# @suaveplan/network-plugin-devtools - Developer-tools plugin for the Suaveplan mesh: NetworkVisualizer (hierarchy/flow/heat-map IR + injectable Renderer + bundled SVG renderer), StateInspector (tree view, ring-buffer history, RFC-6902-style diff via @suaveplan/diff, search/filter), SimulationConfigTool (programmatic topology + condition + event-timeline editor with JSON scenario export/import), PerformanceProfiler (latency p50/p95/p99 distribution, throughput, plugin overhead, memory tracking, two-window EMA regression detection, optimization suggestions), and DevToolsPlugin facade with tier gate and no-op-in-production guard.
|
||||
|
||||
## Summary
|
||||
|
||||
Developer-tools plugin for the Suaveplan mesh. Bundles four subsystems behind
|
||||
a single `NetworkPlugin` lifecycle so engineers can inspect, visualise, and
|
||||
profile the live network from the same node they run their tests against. The
|
||||
plugin is dev-only by contract: a `tierGate` callback decides activation at
|
||||
install time, and `requireActive()` raises `PLUGIN_DEV_DISABLED` so production
|
||||
call sites cannot silently exercise devtools work paths.
|
||||
|
||||
`DevToolsPlugin` ties the four subsystems together:
|
||||
|
||||
- **`NetworkVisualizer`** — renders the mesh as a `VisualizerScene` IR (hierarchy / flow / heat-map modes), supports filtering by tier / latency-group / session / transport, and exports to SVG (bundled `SvgRenderer`) or PNG / canvas / WebGL via injected `Renderer`.
|
||||
- **`StateInspector`** — per-path state browser with bounded ring-buffer history, RFC-6902-style diff via `@suaveplan/diff`, timeline scrubber (`scrub`), and composable `search` over `pathContains` / `peerId` / `sessionId` / version range.
|
||||
- **`SimulationConfigTool`** — programmatic scenario builder (peers, links, conditions, timeline events) with deterministic recording, playback grouping by `atMs`, and Zod-validated JSON export / import for sharing scenarios across teams.
|
||||
- **`PerformanceProfiler`** — latency p50 / p95 / p99 distribution, throughput per path, plugin overhead, memory tracking, two-window EMA + Z-score regression detection against named baselines, and category-tagged optimisation suggestions.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [state-inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [devtools-plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [performance-profiler](./src/profiler/performance-profiler.md) — Captures runtime performance evidence (latency per path, throughput per path, plugin overhead, memory) into bounded rin…
|
||||
- [performance-profiler.suggestions](./src/profiler/performance-profiler.suggestions.md) — Pure-function suggestion engine that consumes profiler ring buffers (latency / overhead / throughput / memory) and emit…
|
||||
- [simulator](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [simulation-config-tool](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [network-visualizer](./src/visualizer/network-visualizer.md) — Renders the live mesh as a `VisualizerScene` intermediate representation (IR) and delegates byte production to an injec…
|
||||
- [svg-renderer](./src/visualizer/svg-renderer.md) — Pure-string SVG renderer that consumes a `VisualizerScene` and produces deterministic UTF-8 SVG bytes without touching…
|
||||
- [error-codes](./src/error-codes.md) — Defines the frozen registry of every failure mode the four devtools subsystems can surface. Centralising the codes here…
|
||||
<!-- end:auto-docs --> - [state-inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [devtools-plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [performance-profiler](./src/profiler/performance-profiler.md) — Captures runtime performance evidence (latency per path, throughput per path, plugin overhead, memory) into bounded rin…
|
||||
- [performance-profiler.suggestions](./src/profiler/performance-profiler.suggestions.md) — Pure-function suggestion engine that consumes profiler ring buffers (latency / overhead / throughput / memory) and emit…
|
||||
- [simulator](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulation-config-tool](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-visualizer](./src/visualizer/network-visualizer.md) — Renders the live mesh as a `VisualizerScene` intermediate representation (IR) and delegates byte production to an injec…
|
||||
- [svg-renderer](./src/visualizer/svg-renderer.md) — Pure-string SVG renderer that consumes a `VisualizerScene` and produces deterministic UTF-8 SVG bytes without touching…
|
||||
- [error-codes](./src/error-codes.md) — Defines the frozen registry of every failure mode the four devtools subsystems can surface. Centralising the codes here…
|
||||
- [state-inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [devtools-plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [performance-profiler](./src/profiler/performance-profiler.md) — Captures runtime performance evidence (latency per path, throughput per path, plugin overhead, memory) into bounded rin…
|
||||
- [performance-profiler.suggestions](./src/profiler/performance-profiler.suggestions.md) — Pure-function suggestion engine that consumes profiler ring buffers (latency / overhead / throughput / memory) and emit…
|
||||
- [simulator](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulation-config-tool](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-visualizer](./src/visualizer/network-visualizer.md) — Renders the live mesh as a `VisualizerScene` intermediate representation (IR) and delegates byte production to an injec…
|
||||
- [svg-renderer](./src/visualizer/svg-renderer.md) — Pure-string SVG renderer that consumes a `VisualizerScene` and produces deterministic UTF-8 SVG bytes without touching…
|
||||
- [error-codes](./src/error-codes.md) — Defines the frozen registry of every failure mode the four devtools subsystems can surface. Centralising the codes here…
|
||||
- [state-inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [devtools-plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [performance-profiler](./src/profiler/performance-profiler.md) — Captures runtime performance evidence (latency per path, throughput per path, plugin overhead, memory) into bounded rin…
|
||||
- [performance-profiler.suggestions](./src/profiler/performance-profiler.suggestions.md) — Pure-function suggestion engine that consumes profiler ring buffers (latency / overhead / throughput / memory) and emit…
|
||||
- [simulator](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [simulation-config-tool](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-visualizer](./src/visualizer/network-visualizer.md) — Renders the live mesh as a `VisualizerScene` intermediate representation (IR) and delegates byte production to an injec…
|
||||
- [svg-renderer](./src/visualizer/svg-renderer.md) — Pure-string SVG renderer that consumes a `VisualizerScene` and produces deterministic UTF-8 SVG bytes without touching…
|
||||
- [error-codes](./src/error-codes.md) — Defines the frozen registry of every failure mode the four devtools subsystems can surface. Centralising the codes here…
|
||||
- [state-inspector](./src/inspector/state-inspector.md) — <!-- begin:auto-docs --> _(no submodules)_- [plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [devtools-plugin](./src/plugin/devtools-plugin.md) — <!-- begin:auto-docs --> _(no submodules)_- [performance-profiler](./src/profiler/performance-profiler.md) — Captures runtime performance evidence (latency per path, throughput per path, plugin overhead, memory) into bounded rin…
|
||||
- [performance-profiler.suggestions](./src/profiler/performance-profiler.suggestions.md) — Pure-function suggestion engine that consumes profiler ring buffers (latency / overhead / throughput / memory) and emit…
|
||||
- [simulator](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_ - [simulation-config-tool](./src/simulator/simulation-config-tool.md) — <!-- begin:auto-docs --> _(no submodules)_- [network-visualizer](./src/visualizer/network-visualizer.md) — Renders the live mesh as a `VisualizerScene` intermediate representation (IR) and delegates byte production to an injec…
|
||||
- [svg-renderer](./src/visualizer/svg-renderer.md) — Pure-string SVG renderer that consumes a `VisualizerScene` and produces deterministic UTF-8 SVG bytes without touching…
|
||||
- [error-codes](./src/error-codes.md) — Defines the frozen registry of every failure mode the four devtools subsystems can surface. Centralising the codes here…
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/network-plugin-devtools
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { NetworkBus, NetworkNode } from "@suaveplan/network";
|
||||
import { DevToolsPlugin } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const node = new NetworkNode({ id: "host-1", networkBus: new NetworkBus() });
|
||||
const devtools = new DevToolsPlugin({
|
||||
tierGate: () => process.env.NODE_ENV !== "production",
|
||||
topology: myTopologyAdapter,
|
||||
stream: myStreamAdapter,
|
||||
inspector: { historyLimit: 1024 },
|
||||
profiler: { regressionZScore: 2.5 },
|
||||
});
|
||||
|
||||
node.use(devtools);
|
||||
await node.start();
|
||||
|
||||
devtools.visualizer.start({ mode: "hierarchy", refreshRate: 30 });
|
||||
const svg = devtools.visualizer.export("svg");
|
||||
|
||||
devtools.inspector.record({
|
||||
path: "session/lobby",
|
||||
value: { players: 3 },
|
||||
version: 1,
|
||||
timestamp: Date.now(),
|
||||
peerId: "host-1",
|
||||
});
|
||||
|
||||
devtools.profiler.recordLatency("host-1->leaf-1", 42);
|
||||
const baseline = devtools.profiler.captureBaseline("v1.0.0");
|
||||
|
||||
devtools.simulator.addPeer({ id: "leaf-2", tier: "tier-2", capacity: 10 });
|
||||
const scenarioJson = devtools.simulator.exportJson();
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`error-codes`](./src/error-codes.md) — `NETWORK_DEV_CODES` registry + `NetworkDevError`.
|
||||
- [`visualizer/network-visualizer`](./src/visualizer/network-visualizer.md) — IR-producing visualisation engine with three modes and pluggable renderer.
|
||||
- [`visualizer/svg-renderer`](./src/visualizer/svg-renderer.md) — bundled dep-free SVG renderer.
|
||||
- [`inspector/state-inspector`](./src/inspector/state-inspector.md) — per-path state browser, history, diff, search, scrubber.
|
||||
- [`simulator/simulation-config-tool`](./src/simulator/simulation-config-tool.md) — programmatic scenario builder with JSON round-trip.
|
||||
- [`profiler/performance-profiler`](./src/profiler/performance-profiler.md) — latency / throughput / overhead / memory tracking with regression detection.
|
||||
- [`profiler/performance-profiler.suggestions`](./src/profiler/performance-profiler.suggestions.md) — heuristic suggestion engine.
|
||||
- [`plugin/devtools-plugin`](./src/plugin/devtools-plugin.md) — facade satisfying `NetworkPlugin` with tier gate and production guard.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# `error-codes` — `NETWORK_DEV_CODES` registry
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the frozen registry of every failure mode the four devtools
|
||||
subsystems can surface. Centralising the codes here means consumers can
|
||||
match on a known string set, observability backends get stable
|
||||
`NET_DEV.*` labels, and i18n message bundles can be authored against a
|
||||
fixed keyset rather than free-form errors.
|
||||
|
||||
The registry is the contract: each subsystem (`NetworkVisualizer`,
|
||||
`StateInspector`, `SimulationConfigTool`, `PerformanceProfiler`, and the
|
||||
`DevToolsPlugin` facade) raises a `NetworkDevError` carrying one of
|
||||
these codes whenever an invariant is violated, an input fails
|
||||
validation, or an injected callback misbehaves. Adding a new failure
|
||||
mode means adding a new registry entry first; no ad-hoc strings.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `NETWORK_DEV_CODES` object literal — `Object.isFrozen()` returns `true`.
|
||||
- All entries prefixed `NET_DEV.` via `defineCodes("NET_DEV", ...)`.
|
||||
- 20 codes spanning visualizer, inspector, simulator, profiler, and plugin facade.
|
||||
- `NetworkDevError extends SuaveplanError<NetworkDevErrorCode>` preserves cause chains, context maps, OTel-friendly metadata, and i18n message keys.
|
||||
- Side-effect-imported from `src/index.ts` so subpath consumers register the code namespace at module load.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NETWORK_DEV_CODES, NetworkDevError } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
throw new NetworkDevError(
|
||||
NETWORK_DEV_CODES.INSPECTOR_PATH_NOT_FOUND,
|
||||
"inspector.unknown-path",
|
||||
{ context: { path: "game/positions/player1" } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NETWORK_DEV_CODES,
|
||||
NetworkDevError,
|
||||
} from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
function ensurePeer(peerId: string, known: ReadonlySet<string>): void {
|
||||
if (!known.has(peerId)) {
|
||||
throw new NetworkDevError(
|
||||
NETWORK_DEV_CODES.SIMULATOR_PEER_NOT_FOUND,
|
||||
"simulator.peer.unknown",
|
||||
{
|
||||
context: { peerId, knownCount: known.size },
|
||||
cause: new Error("topology lookup miss"),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `NETWORK_DEV_CODES` — frozen `Record<Key, "NET_DEV.<Key>">`.
|
||||
- `NetworkDevErrorCode` — string-literal union of every value.
|
||||
- `class NetworkDevError extends SuaveplanError<NetworkDevErrorCode>` — constructor `(code, messageKey, options?)`. Sets `name = "NetworkDevError"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is intentionally additive — every code lives forever once shipped, even if a subsystem stops throwing it, because consumers may still match on it. Removing a code requires an OpenSpec change and a new major version. The `SuaveplanError` base preserves `cause` per the ES error spec, so wrapping a transient I/O failure (renderer pipe broken, scenario JSON parse failure) with a typed code does not destroy the underlying diagnostic. Tests in the sibling spec-conformance file pin the prefix, the freeze invariant, and every key by name so accidental rename is impossible.
|
||||
@@ -0,0 +1,97 @@
|
||||
# `inspector/state-inspector` — `StateInspector`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Per-path state browser for the synchronised mesh state. Records
|
||||
snapshots into a bounded ring buffer keyed by `path`, exposes a tree of
|
||||
known paths plus per-path history, computes RFC-6902-style diffs
|
||||
between any two recorded versions via `@suaveplan/diff`, and supports
|
||||
search by path-substring, peer, session, and version range.
|
||||
|
||||
The inspector is the read-side companion to `network-plugin-state`
|
||||
without taking a runtime dependency on it — consumers feed
|
||||
`StateSnapshot` records from whatever state-coordination plugin they
|
||||
use, so the inspector is also useful for plugins outside the canonical
|
||||
state plugin (game state, presence, custom CRDT implementations).
|
||||
|
||||
## Features
|
||||
|
||||
- Bounded per-path ring buffer (default 256, configurable up to 10 000).
|
||||
- `record(snapshot)` Zod-validates input and rejects invalid frames with `INSPECTOR_DIFF_FAILED`.
|
||||
- `inspect(path)` — current value plus full history.
|
||||
- `list()` — sorted list of all known paths.
|
||||
- `diff(path, fromVersion, toVersion)` — produces `DiffView` with `added` / `removed` / `changed` JSON Pointers and a numeric `summary`.
|
||||
- `scrub(path, atVersion)` — timeline scrubber returns the latest snapshot ≤ `atVersion`.
|
||||
- `search(filter)` — composable `pathContains` / `peerId` / `sessionId` / `minVersion` / `maxVersion`.
|
||||
- OpenTelemetry counters for record + diff operations.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { StateInspector } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const inspector = new StateInspector();
|
||||
inspector.record({
|
||||
path: "game/positions/player1",
|
||||
value: { x: 0, y: 0 },
|
||||
version: 1,
|
||||
timestamp: Date.now(),
|
||||
peerId: "host-1",
|
||||
});
|
||||
const view = inspector.inspect("game/positions/player1");
|
||||
console.log(view.current.value);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { StateInspector } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const inspector = new StateInspector({ historyLimit: 1024 });
|
||||
for (const snap of stream.snapshots) inspector.record(snap);
|
||||
|
||||
const change = inspector.diff("session/lobby", 14, 17);
|
||||
console.log(change.summary); // { addedCount: 2, removedCount: 0, changedCount: 1 }
|
||||
|
||||
const at = inspector.scrub("session/lobby", 15);
|
||||
console.log(at.peerId, at.value);
|
||||
|
||||
const recent = inspector.search({
|
||||
pathContains: "session/",
|
||||
peerId: "host-eu-1",
|
||||
minVersion: 10,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class StateInspector` — constructor `(config?: InspectorConfig)`. `historyLimit` defaults to 256.
|
||||
- `record(snapshot: StateSnapshot): void` — throws `INSPECTOR_DIFF_FAILED` on invalid input.
|
||||
- `inspect(path: string): StateView` — throws `INSPECTOR_PATH_NOT_FOUND` when path absent.
|
||||
- `list(): readonly string[]` — sorted ascending.
|
||||
- `has(path: string): boolean`.
|
||||
- `clear(path?: string): void` — clears single path or every path.
|
||||
- `diff(path, fromVersion, toVersion): DiffView` — throws `INSPECTOR_INVALID_RANGE` when versions are equal or missing.
|
||||
- `scrub(path, atVersion): StateSnapshot` — throws `INSPECTOR_PATH_NOT_FOUND` for unknown paths and `INSPECTOR_HISTORY_EMPTY` when no snapshot satisfies the cap.
|
||||
- `search(filter: SearchFilter): readonly StateSnapshot[]` — AND-combines populated fields.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The inspector is intentionally write-once-per-version — the ring is
|
||||
append-only and a new snapshot for an existing version simply pushes
|
||||
another entry rather than overwriting, because vector-clock state
|
||||
systems can legitimately produce concurrent snapshots at the same
|
||||
version from different peers; keeping both lets `search` find them. The
|
||||
diff path delegates to `@suaveplan/diff` for the heavy work and only
|
||||
classifies the resulting operations into added/removed/changed buckets
|
||||
so consumers can render badge counts without re-inspecting the patch.
|
||||
`move`/`copy` ops collapse into `changed` because they imply both ends
|
||||
mutate; pure-format consumers needing the full patch should call
|
||||
`@suaveplan/diff`'s `diff()` directly. `scrub` returns the latest
|
||||
snapshot whose version is ≤ the target so timeline scrubbers naturally
|
||||
clamp to a recorded frame; this is friendlier than throwing on
|
||||
sub-version queries which would require the consumer to first call
|
||||
`list()` and bisect.
|
||||
@@ -0,0 +1,96 @@
|
||||
# `plugin/devtools-plugin` — `DevToolsPlugin`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Single `NetworkPlugin` that bundles every devtools subsystem
|
||||
(`NetworkVisualizer`, `StateInspector`, `SimulationConfigTool`,
|
||||
`PerformanceProfiler`) behind one lifecycle, governs activation via a
|
||||
`tierGate` callback, and exposes each subsystem as a public field so
|
||||
consumers can compose without going through the plugin abstraction.
|
||||
|
||||
The plugin is dev-only by contract: production builds wire `tierGate`
|
||||
to `() => false` so `start()` short-circuits, no subsystem fires, and
|
||||
`requireActive()` raises `PLUGIN_DEV_DISABLED` for any call site that
|
||||
needs a hard guarantee. This keeps the devtools dependency safe to
|
||||
include in production bundles (zero side effects, zero overhead) while
|
||||
still letting downstream tooling import the same package without
|
||||
conditional code paths.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements the canonical `NetworkPlugin` lifecycle: `install` / `start` / `stop` / `dispose`.
|
||||
- `tierGate` decides activation at `install()` time; rejection is recorded as `PLUGIN_TIER_GATE_REJECTED` for observability.
|
||||
- Direct subsystem access: `plugin.visualizer`, `plugin.inspector`, `plugin.simulator`, `plugin.profiler`.
|
||||
- `requireActive()` raises `PLUGIN_DEV_DISABLED` when consumers want a hard production-build assertion.
|
||||
- `dispose()` resets every subsystem (clears inspector, resets profiler, resets simulator scenario, stops visualizer if running).
|
||||
- Idempotent stop and dispose semantics — safe to call multiple times.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NetworkBus, NetworkNode } from "@suaveplan/network";
|
||||
import { DevToolsPlugin } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const node = new NetworkNode({ id: "host-1", networkBus: new NetworkBus() });
|
||||
const devtools = new DevToolsPlugin({
|
||||
tierGate: () => process.env.NODE_ENV !== "production",
|
||||
topology: myTopologyAdapter,
|
||||
stream: myStreamAdapter,
|
||||
});
|
||||
node.use(devtools);
|
||||
await node.start();
|
||||
devtools.visualizer.start({ mode: "hierarchy", refreshRate: 30 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { NetworkBus, NetworkNode } from "@suaveplan/network";
|
||||
import { DevToolsPlugin } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const devtools = new DevToolsPlugin({
|
||||
tierGate: () => true,
|
||||
topology,
|
||||
stream,
|
||||
visualizer: { renderer: customPngRenderer, width: 1200, height: 900 },
|
||||
inspector: { historyLimit: 1024 },
|
||||
profiler: { windowSize: 4096, regressionZScore: 2.5, regressionWarnPct: 15 },
|
||||
simulator: { idGenerator: () => crypto.randomUUID() },
|
||||
});
|
||||
|
||||
node.use(devtools);
|
||||
await node.start();
|
||||
devtools.requireActive();
|
||||
devtools.profiler.recordLatency("hot-path", 42);
|
||||
devtools.simulator.addPeer({ id: "peer-1", tier: "tier-2", capacity: 10 });
|
||||
const baseline = devtools.profiler.captureBaseline("v1.0.0");
|
||||
const regressions = devtools.profiler.compare(baseline.id);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class DevToolsPlugin implements NetworkPlugin`.
|
||||
- Constructor `(config: DevToolsPluginConfig)`. Required: `tierGate`, `topology`, `stream`. Optional sub-config blocks for each subsystem.
|
||||
- Public fields: `visualizer`, `inspector`, `simulator`, `profiler`.
|
||||
- Lifecycle: `install(node)`, `start()`, `stop()`, `dispose()`.
|
||||
- Status: `isStarted()`, `isActive()`, `host()`, `tierRejection()`.
|
||||
- Guard: `requireActive()` throws `PLUGIN_DEV_DISABLED` if the tier gate rejected activation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `tierGate` is intentionally a function rather than a boolean so
|
||||
consumers can re-evaluate at every `install()` — useful for tests that
|
||||
want to flip activation without rebuilding the plugin. The plugin
|
||||
records the rejection as a code rather than swallowing it because
|
||||
"why is the bridge silent?" is a frequent observability question, and
|
||||
the answer should be a typed string (`PLUGIN_TIER_GATE_REJECTED`)
|
||||
queryable from logs. Sub-subsystem disposal in `dispose()` is gated on
|
||||
`active` so production-disabled installs don't pay the reset cost on
|
||||
shutdown — every subsystem's `clear()` / `reset()` / `newScenario()`
|
||||
operation is cheap, but skipping the work keeps the dispose path a
|
||||
true no-op when the plugin never activated. The optional config blocks
|
||||
spread directly into each subsystem constructor; no wrapping or
|
||||
adapter layer mediates between them.
|
||||
@@ -0,0 +1,92 @@
|
||||
# `profiler/performance-profiler` — `PerformanceProfiler`
|
||||
|
||||
## Purpose
|
||||
|
||||
Captures runtime performance evidence (latency per path, throughput per
|
||||
path, plugin overhead, memory) into bounded ring buffers, computes
|
||||
percentile distributions, captures named baselines, detects regressions
|
||||
between current and baseline runs, and surfaces actionable optimisation
|
||||
suggestions for engineers triaging performance work.
|
||||
|
||||
The profiler is the second half of the devtools observability story —
|
||||
the visualizer shows you what the network LOOKS like; the profiler
|
||||
quantifies how it BEHAVES. Both are designed to compose with the
|
||||
production-grade `network-plugin-monitor` collector — the profiler
|
||||
focuses on developer-time analysis (regressions, suggestions) rather
|
||||
than alerting.
|
||||
|
||||
## Features
|
||||
|
||||
- `recordLatency` / `recordThroughput` / `recordPluginOverhead` / `recordMemory` ingest.
|
||||
- Per-path bounded ring (default 256, configurable).
|
||||
- `latencyDistribution(path)` exposes p50 / p95 / p99 + min / max / mean / count.
|
||||
- `report()` aggregates everything into a single `PerformanceReport`.
|
||||
- `captureBaseline(id)` snapshots the current report for later comparison; baselines are immutable, duplicate-id raises `PROFILER_BASELINE_DUPLICATE`.
|
||||
- `compare(baselineId)` produces a `Regression[]` ranked by Z-score across p50 / p95 / p99 latency and message-throughput drops.
|
||||
- `suggestions()` emits `OptimizationSuggestion`s for tail-latency, plugin overhead, heap pressure, and zero-throughput anomalies.
|
||||
- Configurable thresholds: `regressionZScore` (default 2.0), `regressionWarnPct` (default 10).
|
||||
- OTel counters: `devtools.profiler.sample_total{kind}`, `devtools.profiler.report_total`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PerformanceProfiler } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const profiler = new PerformanceProfiler();
|
||||
profiler.recordLatency("host-1->leaf-1", 42);
|
||||
profiler.recordLatency("host-1->leaf-1", 51);
|
||||
const dist = profiler.latencyDistribution("host-1->leaf-1");
|
||||
console.log(dist.p99);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PerformanceProfiler } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const profiler = new PerformanceProfiler({
|
||||
windowSize: 1024,
|
||||
regressionZScore: 2.5,
|
||||
regressionWarnPct: 15,
|
||||
clock: () => deterministicClock.now(),
|
||||
});
|
||||
|
||||
for (const sample of warmupRun) profiler.recordLatency(sample.path, sample.ms);
|
||||
const baseline = profiler.captureBaseline("v1.4.2");
|
||||
|
||||
profiler.reset();
|
||||
for (const sample of regressionRun) profiler.recordLatency(sample.path, sample.ms);
|
||||
const regressions = profiler.compare(baseline.id);
|
||||
for (const r of regressions) {
|
||||
console.log(`${r.severity} ${r.metric} on ${r.path}: +${r.deltaPct.toFixed(1)}%`);
|
||||
}
|
||||
for (const s of profiler.suggestions()) {
|
||||
console.log(`[${s.category}] ${s.subject}: ${s.message}`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class PerformanceProfiler` — constructor `(config?: ProfilerConfig)`. Throws `PROFILER_INVALID_THRESHOLD` on non-positive `windowSize`/`regressionZScore`/`regressionWarnPct`.
|
||||
- Recorders: `recordLatency(path, ms)`, `recordThroughput(path, bps, mps)`, `recordPluginOverhead(name, cpuMs)`, `recordMemory(used, total)`.
|
||||
- Queries: `latencyDistribution(path)` (throws `PROFILER_NO_SAMPLES` when empty), `report()`.
|
||||
- Baselines: `captureBaseline(id)`, `listBaselines()`, `deleteBaseline(id)` (throws `PROFILER_NO_BASELINE`).
|
||||
- Comparison: `compare(baselineId): readonly Regression[]` (throws `PROFILER_NO_BASELINE` when absent).
|
||||
- Heuristics: `suggestions(): readonly OptimizationSuggestion[]`.
|
||||
- Lifecycle: `reset()` clears every sample buffer (baselines preserved).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Z-score approximation (`deltaPct / max(1, base * 0.1)`) is a
|
||||
deliberately simple proxy for a true two-window statistical test — full
|
||||
Welch's t-test requires storing both windows' raw distributions, and
|
||||
the profiler is intentionally light enough to be left running in
|
||||
developer builds. The proxy is monotonic in delta and falls back
|
||||
sensibly when the baseline is small. Severity ladder: error when
|
||||
Z ≥ threshold; warn when delta ≥ `warnPct`; info when delta ≥ half of
|
||||
`warnPct`. Throughput regressions track the DROP direction — a fall in
|
||||
messages/sec is the regression to surface, not a rise. The optional
|
||||
suggestions module is split into a sibling file so the regression
|
||||
math and the heuristic strings don't share a 300-line budget; both
|
||||
remain hand-tuned and audited rather than ML-driven, because devtools
|
||||
output must be explainable to engineers.
|
||||
@@ -0,0 +1,74 @@
|
||||
# `profiler/performance-profiler.suggestions` — heuristic suggestion engine
|
||||
|
||||
## Purpose
|
||||
|
||||
Pure-function suggestion engine that consumes profiler ring buffers
|
||||
(latency / overhead / throughput / memory) and emits an
|
||||
`OptimizationSuggestion[]` with category-tagged advice for engineers
|
||||
triaging performance work. Split out of `performance-profiler.ts` so
|
||||
the regression-math hot path and the developer-facing heuristic strings
|
||||
are separately auditable and can grow independently.
|
||||
|
||||
## Features
|
||||
|
||||
- Tail-latency detection: flags paths where p99 > 2×p50 AND p99 > 100ms.
|
||||
- Plugin-overhead detection: flags plugins whose mean CPU per invocation exceeds 5ms.
|
||||
- Heap-pressure detection: flags processes whose heap usage exceeds 85% of `heapTotalBytes`.
|
||||
- Zero-throughput detection: flags paths reporting `messagesPerSec === 0` (likely a dead transport).
|
||||
- Pure function — no I/O, no global state, no clock dependency.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { collectSuggestions } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const suggestions = collectSuggestions(latencyMap, overheadMap, throughputMap, memorySample);
|
||||
for (const s of suggestions) console.log(`[${s.category}] ${s.subject}: ${s.message}`);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
PerformanceProfiler,
|
||||
collectSuggestions,
|
||||
} from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const profiler = new PerformanceProfiler();
|
||||
profiler.recordLatency("hot-path", 100);
|
||||
profiler.recordLatency("hot-path", 250);
|
||||
profiler.recordPluginOverhead("validator", 12);
|
||||
profiler.recordMemory(900, 1000);
|
||||
|
||||
// `profiler.suggestions()` is a thin facade over `collectSuggestions`,
|
||||
// but call the helper directly when consuming raw maps from another
|
||||
// source (e.g. an upstream collector you bridge in tests).
|
||||
const direct = collectSuggestions(
|
||||
new Map([["hot-path", [{ path: "hot-path", latencyMs: 250, recordedAt: 0 }]]]),
|
||||
new Map(),
|
||||
new Map(),
|
||||
null
|
||||
);
|
||||
console.log(direct);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `collectSuggestions(latency, overhead, throughput, memory): readonly OptimizationSuggestion[]` — pure; never throws.
|
||||
- `latency: ReadonlyMap<string, ReadonlyArray<LatencySample>>`
|
||||
- `overhead: ReadonlyMap<string, ReadonlyArray<PluginOverheadSample>>`
|
||||
- `throughput: ReadonlyMap<string, ThroughputSample>`
|
||||
- `memory: MemorySample | null`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Thresholds are hard-coded and explained inline (`TAIL_RATIO`,
|
||||
`TAIL_FLOOR_MS`, `OVERHEAD_AVG_MS`, `HEAP_USAGE_LIMIT`). Empirical
|
||||
tuning lives here intentionally so engineers reading suggestions can
|
||||
trace the cutoffs without going through the configurable
|
||||
regression thresholds in `performance-profiler.ts` — the two paths
|
||||
serve different audiences. The local `quickDistribution` helper
|
||||
duplicates a tiny slice of the percentile logic in
|
||||
`performance-profiler.ts` rather than importing it because the helper
|
||||
needs only p50 and p99 and avoiding the import keeps this module
|
||||
dep-free and easy to inline-test.
|
||||
@@ -0,0 +1,117 @@
|
||||
# `simulator/simulation-config-tool` — `SimulationConfigTool`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Programmatic builder for mesh simulation scenarios. NOT a UI — exposes
|
||||
a deterministic API for adding/removing peers and links, defining
|
||||
time-windowed network conditions (latency / loss / jitter / bandwidth /
|
||||
partition windows), scheduling timeline events, recording an observed
|
||||
event stream, replaying recordings as `PlaybackTick`s grouped by
|
||||
timestamp, and round-tripping the entire scenario through a
|
||||
JSON-schema-validated artifact for sharing across teams.
|
||||
|
||||
The tool intentionally has zero rendering or simulation logic — it is
|
||||
the configuration surface that feeds into
|
||||
`@suaveplan/network-plugin-simulation`'s simulation engine. Decoupling
|
||||
the editor from the engine means scenario JSON can be authored,
|
||||
validated, and version-controlled without spinning up an actual
|
||||
simulation runtime.
|
||||
|
||||
## Features
|
||||
|
||||
- `addPeer` / `removePeer` / `listPeers` with Zod boundary; `SIMULATOR_PEER_DUPLICATE` on collision; cascading link removal on peer deletion.
|
||||
- `addLink` validates endpoint existence and surfaces `SIMULATOR_PEER_NOT_FOUND` for dangling references.
|
||||
- `setCondition` / `clearCondition` / `listConditions` with `endMs > startMs` Zod refine; sorted output.
|
||||
- `addEvent` / `listEvents` for `peer-join`, `peer-leave`, `link-up`, `link-down`, `condition-apply`, `condition-clear` events; recording capture appends in parallel.
|
||||
- `startRecording` / `stopRecording` produces a `ScenarioRecording` keyed by current scenario id and a clock-stamped `recordedAtMs`.
|
||||
- `loadPlayback` enforces scenario-id match before replay; `playback()` groups events by `atMs` for tick-accurate replay.
|
||||
- `exportJson` / `importJson` with full Zod validation; `JSON.parse` failures and schema failures both surface as `SIMULATOR_SCENARIO_INVALID`.
|
||||
- Injectable `clock` and `idGenerator` for fully deterministic tests.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SimulationConfigTool } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const tool = new SimulationConfigTool();
|
||||
tool.addPeer({ id: "host-1", tier: "tier-0", capacity: 100 });
|
||||
tool.addPeer({ id: "leaf-1", tier: "tier-2", capacity: 10 });
|
||||
tool.addLink({
|
||||
from: "host-1",
|
||||
to: "leaf-1",
|
||||
latencyMs: 50,
|
||||
jitterMs: 5,
|
||||
lossRate: 0.01,
|
||||
bandwidthKbps: 10_000,
|
||||
});
|
||||
const scenario = tool.toScenario();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { SimulationConfigTool } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const tool = new SimulationConfigTool({
|
||||
clock: () => 1_700_000_000_000,
|
||||
idGenerator: () => "scen-001",
|
||||
});
|
||||
tool.setMetadata({
|
||||
name: "client-surge",
|
||||
description: "100 leaves join in 5 seconds",
|
||||
durationMs: 30_000,
|
||||
});
|
||||
tool.startRecording();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
tool.addPeer({ id: `leaf-${i}`, tier: "tier-2", capacity: 10 });
|
||||
tool.addEvent({
|
||||
id: `e-${i}`,
|
||||
atMs: i * 50,
|
||||
kind: "peer-join",
|
||||
target: `leaf-${i}`,
|
||||
});
|
||||
}
|
||||
const recording = tool.stopRecording();
|
||||
|
||||
const replay = new SimulationConfigTool({ idGenerator: () => "scen-001" });
|
||||
replay.loadPlayback(recording);
|
||||
for (const tick of replay.playback()) {
|
||||
console.log(`@${tick.atMs}ms`, tick.events.length, "events");
|
||||
}
|
||||
|
||||
const json = tool.exportJson();
|
||||
await Bun.write("./client-surge.json", json);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class SimulationConfigTool` — constructor `(options?)`. `options.clock` defaults to `Date.now`; `options.idGenerator` defaults to a base-36 random.
|
||||
- Peer ops: `addPeer(SimulatedPeer)`, `removePeer(id)`, `listPeers()`. Throws `SIMULATOR_PEER_DUPLICATE` / `SIMULATOR_PEER_NOT_FOUND` / `SIMULATOR_INVALID_CONDITION`.
|
||||
- Link ops: `addLink(SimulatedLink)`, `listLinks()`. Throws `SIMULATOR_PEER_NOT_FOUND` when endpoints missing.
|
||||
- Condition ops: `setCondition(NetworkCondition)`, `clearCondition(id)`, `listConditions()`. Throws `SIMULATOR_INVALID_CONDITION` on schema failure or unknown id.
|
||||
- Event ops: `addEvent(ScenarioEvent)`, `listEvents()`.
|
||||
- Metadata: `setMetadata({ name?, description?, durationMs? })`. Bad duration → `SIMULATOR_INVALID_CONDITION`.
|
||||
- Recording: `startRecording()`, `stopRecording(): ScenarioRecording`. Stop without start → `SIMULATOR_RECORDING_INACTIVE`.
|
||||
- Playback: `loadPlayback(recording)`, `playback(): readonly PlaybackTick[]`. Mismatched scenario → `SIMULATOR_SCENARIO_INVALID`; playback before load → `SIMULATOR_PLAYBACK_INACTIVE`.
|
||||
- Round-trip: `exportJson(): string`, `importJson(json): void`. JSON parse / schema fail → `SIMULATOR_SCENARIO_INVALID`.
|
||||
- `toScenario(): SimulationScenario`, `scenarioIdValue()`, `newScenario()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The tool keeps mutable internal collections but every public listing
|
||||
method returns a defensive copy so consumers cannot mutate the internal
|
||||
state by holding the reference. Validation happens at the API boundary,
|
||||
not on every internal mutation, because the tool re-validates the full
|
||||
scenario on `importJson` and trusting the post-Zod data internally
|
||||
keeps the hot path cheap. Cascade-on-delete for `removePeer` is
|
||||
intentional — leaving dangling links would force every consumer to
|
||||
re-scan after deletion. The `loadPlayback` scenario-id guard prevents
|
||||
the common foot-gun of replaying a recording against the wrong
|
||||
scenario; consumers wanting to migrate a recording call `newScenario()`
|
||||
followed by `importJson` and only then `loadPlayback`. The `validate`
|
||||
helper accepts a structural `SafeParser` shape rather than a Zod schema
|
||||
type so unit tests can substitute lightweight validators when stress
|
||||
testing input fan-in.
|
||||
@@ -0,0 +1,73 @@
|
||||
# `types/inspector.types` — inspector type contracts
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared type definitions for the `StateInspector` subsystem. Producers
|
||||
(state-coordination plugins, tests, snapshot recorders) materialise
|
||||
`StateSnapshot` objects against this contract; consumers (UIs, log
|
||||
shippers, replay tools) consume `StateView` and `DiffView` projections
|
||||
without importing the implementation file.
|
||||
|
||||
## Features
|
||||
|
||||
- `StateSnapshot` carrying `path` / `value` / `version` / `timestamp` / `peerId` / optional `sessionId`.
|
||||
- `StateView` returned from `inspect(path)` — current snapshot plus full ring.
|
||||
- `InspectorConfig` with optional `historyLimit` (defaults to 256).
|
||||
- `DiffView` carrying JSON-Pointer arrays for added / removed / changed plus a `DiffSummary` count badge.
|
||||
- `SearchFilter` AND-combining `pathContains`, `peerId`, `sessionId`, `minVersion`, `maxVersion`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { StateSnapshot } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
function snapshotOf(path: string, value: unknown, version: number): StateSnapshot {
|
||||
return {
|
||||
path,
|
||||
value: value as StateSnapshot["value"],
|
||||
version,
|
||||
timestamp: Date.now(),
|
||||
peerId: "host-1",
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { DiffView, SearchFilter, StateView } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
function badge(view: DiffView): string {
|
||||
const { addedCount, removedCount, changedCount } = view.summary;
|
||||
return `+${addedCount} -${removedCount} ~${changedCount}`;
|
||||
}
|
||||
|
||||
const filter: SearchFilter = {
|
||||
pathContains: "session/",
|
||||
peerId: "host-eu-1",
|
||||
minVersion: 100,
|
||||
maxVersion: 200,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface StateSnapshot { path, value (JsonValue), version, timestamp, peerId, sessionId? }`.
|
||||
- `interface StateView { path, current: StateSnapshot, history: readonly StateSnapshot[] }`.
|
||||
- `interface InspectorConfig { historyLimit? }`.
|
||||
- `interface DiffView { path, fromVersion, toVersion, added, removed, changed, summary }`.
|
||||
- `interface DiffSummary { addedCount, removedCount, changedCount }`.
|
||||
- `interface SearchFilter { pathContains?, peerId?, sessionId?, minVersion?, maxVersion? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`value` is typed as `JsonValue` (re-exported from `@suaveplan/diff`) so
|
||||
diff and patch operations have a stable boundary. Using `JsonValue`
|
||||
rather than `unknown` means the inspector cannot accidentally store
|
||||
non-serialisable values (functions, symbols, class instances) — those
|
||||
would silently break the diff path. `version` is a number rather than
|
||||
a vector clock because the inspector is per-path; cross-path causality
|
||||
is the responsibility of the upstream state plugin. The optional
|
||||
`sessionId` field is omitted (not `undefined`) to satisfy
|
||||
`exactOptionalPropertyTypes`; consumers building snapshots conditionally
|
||||
should spread it only when populated.
|
||||
@@ -0,0 +1,80 @@
|
||||
# `types/profiler.types` — profiler type contracts
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared type definitions for the `PerformanceProfiler` subsystem.
|
||||
Producers (instrumented call sites, telemetry pipelines, tests)
|
||||
materialise sample shapes against these contracts; consumers (report
|
||||
viewers, regression dashboards, suggestion renderers) read
|
||||
`PerformanceReport`, `Baseline`, `Regression`, and
|
||||
`OptimizationSuggestion` projections without importing the
|
||||
implementation file.
|
||||
|
||||
## Features
|
||||
|
||||
- Four sample shapes: `LatencySample`, `ThroughputSample`, `PluginOverheadSample`, `MemorySample`.
|
||||
- `LatencyDistribution` — count + min / max / mean + p50 / p95 / p99.
|
||||
- `PerformanceReport` — typed read-only `Map`s of every aggregate.
|
||||
- `Baseline` — frozen capture for later `compare(baselineId)` calls.
|
||||
- `Regression` + `RegressionSeverity` — discriminated by metric (`p50` / `p95` / `p99` / `throughput`) and severity ladder (`info` / `warn` / `error`).
|
||||
- `OptimizationSuggestion` with category enum (`latency` / `throughput` / `overhead` / `memory`).
|
||||
- `ProfilerConfig` injectable thresholds + clock.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { LatencyDistribution } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
function summary(d: LatencyDistribution): string {
|
||||
return `n=${d.count} p50=${d.p50.toFixed(1)} p99=${d.p99.toFixed(1)}`;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
Baseline,
|
||||
PerformanceReport,
|
||||
Regression,
|
||||
} from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
function report(b: Baseline, current: PerformanceReport): string {
|
||||
const aged = (Date.now() - b.capturedAt) / 60_000;
|
||||
return `baseline ${b.id} (age ${aged.toFixed(1)} min)\n` +
|
||||
`${current.latencyByPath.size} paths tracked`;
|
||||
}
|
||||
|
||||
function severityBadge(r: Regression): string {
|
||||
return `[${r.severity}] ${r.metric} on ${r.path}: +${r.deltaPct.toFixed(1)}%`;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface LatencySample { path, latencyMs, recordedAt }`.
|
||||
- `interface ThroughputSample { path, bytesPerSec, messagesPerSec, recordedAt }`.
|
||||
- `interface PluginOverheadSample { pluginName, cpuMs, recordedAt }`.
|
||||
- `interface MemorySample { heapUsedBytes, heapTotalBytes, recordedAt }`.
|
||||
- `interface LatencyDistribution { count, p50, p95, p99, min, max, mean }`.
|
||||
- `interface PerformanceReport { latencyByPath, throughputByPath, pluginOverhead, memory, generatedAt }`.
|
||||
- `interface Baseline { id, capturedAt, report }`.
|
||||
- `type RegressionSeverity = "info" | "warn" | "error"`.
|
||||
- `interface Regression { path, metric, baseline, current, deltaPct, zScore, severity }`.
|
||||
- `interface OptimizationSuggestion { subject, category, message, evidence }`.
|
||||
- `interface ProfilerConfig { windowSize?, regressionZScore?, regressionWarnPct?, clock? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`LatencyDistribution` is intentionally flat (no nested `percentiles`
|
||||
sub-object) so a downstream metrics adapter can map every field to a
|
||||
gauge without re-shaping. The choice of p50 / p95 / p99 mirrors the
|
||||
Prometheus / OpenTelemetry industry standard; emitting p99.9 / max-only
|
||||
distributions would force a wider sample buffer and fewer engineers
|
||||
intuit those numbers reliably. `Regression.severity` is a closed
|
||||
three-value union — adding a level requires an OpenSpec change because
|
||||
downstream alert wiring commits to it. `OptimizationSuggestion.subject`
|
||||
is intentionally a free string (not a path or peer id) because
|
||||
suggestion subjects span paths, plugin names, and synthetic categories
|
||||
like `"process"` for memory pressure; downstream renderers should
|
||||
treat it as a display label, not a navigable identifier.
|
||||
@@ -0,0 +1,98 @@
|
||||
# `types/simulator.types` — simulator type contracts
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared type definitions for the `SimulationConfigTool` subsystem.
|
||||
Producers (scenario authors, recording capture, JSON importers) and
|
||||
consumers (the simulation engine downstream, scenario serialisers, UI
|
||||
editors) both reference these contracts. Keeping the types separate
|
||||
from the implementation file means consumers can author scenarios
|
||||
purely against the types without instantiating the tool.
|
||||
|
||||
## Features
|
||||
|
||||
- `SimulatorTier` mirrors mesh tiers (`tier-0` / `tier-1` / `tier-2`).
|
||||
- `SimulatedPeer` — id / tier / optional region / capacity.
|
||||
- `SimulatedLink` — endpoints + latency / jitter / loss / bandwidth.
|
||||
- `NetworkCondition` — time-windowed `[startMs, endMs]` shape with optional `partitioned` peer ids.
|
||||
- `ScenarioEvent` + `ScenarioEventKind` — six kinds covering peer/link lifecycle and condition application.
|
||||
- `SimulationScenario` — the top-level artifact serialised by `exportJson`.
|
||||
- `ScenarioRecording` / `PlaybackTick` — capture + replay shapes.
|
||||
- `SimulationConfigToolOptions` — injectable `clock` and `idGenerator`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { SimulatedPeer } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const peer: SimulatedPeer = {
|
||||
id: "leaf-1",
|
||||
tier: "tier-2",
|
||||
capacity: 10,
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
NetworkCondition,
|
||||
ScenarioEvent,
|
||||
SimulationScenario,
|
||||
} from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const condition: NetworkCondition = {
|
||||
id: "spike",
|
||||
startMs: 5_000,
|
||||
endMs: 10_000,
|
||||
latencyMs: 200,
|
||||
jitterMs: 30,
|
||||
lossRate: 0.2,
|
||||
bandwidthKbps: 500,
|
||||
partitioned: ["leaf-7"],
|
||||
};
|
||||
|
||||
const event: ScenarioEvent = {
|
||||
id: "ev-1",
|
||||
atMs: 5_000,
|
||||
kind: "condition-apply",
|
||||
target: "spike",
|
||||
payload: { reason: "rolling-restart" },
|
||||
};
|
||||
|
||||
const scenario: SimulationScenario = {
|
||||
id: "client-surge",
|
||||
name: "Client surge",
|
||||
description: "100 leaves join in 5s",
|
||||
peers: [],
|
||||
links: [],
|
||||
conditions: [condition],
|
||||
events: [event],
|
||||
durationMs: 30_000,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type SimulatorTier = "tier-0" | "tier-1" | "tier-2"`.
|
||||
- `interface SimulatedPeer { id, tier, region?, capacity }`.
|
||||
- `interface SimulatedLink { from, to, latencyMs, jitterMs, lossRate, bandwidthKbps }`.
|
||||
- `interface NetworkCondition { id, startMs, endMs, latencyMs, jitterMs, lossRate, bandwidthKbps, partitioned? }`.
|
||||
- `type ScenarioEventKind = "peer-join" | "peer-leave" | "link-up" | "link-down" | "condition-apply" | "condition-clear"`.
|
||||
- `interface ScenarioEvent { id, atMs, kind, target, payload? }`.
|
||||
- `interface SimulationScenario { id, name, description, peers, links, conditions, events, durationMs }`.
|
||||
- `interface ScenarioRecording { scenarioId, recordedAtMs, events }`.
|
||||
- `interface PlaybackTick { atMs, events }`.
|
||||
- `interface SimulationConfigToolOptions { clock?, idGenerator? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `payload` field on `ScenarioEvent` is intentionally a
|
||||
`Record<string, string | number | boolean>` rather than `JsonValue` —
|
||||
JSON-import safety is much easier to validate against scalar payloads,
|
||||
and engineers editing scenarios by hand rarely need nested structures.
|
||||
If a future engine needs richer payloads, a versioned schema upgrade
|
||||
in `simulation-config-tool.schemas.ts` is the migration path. Tier
|
||||
strings duplicate `VisualizerTier` rather than aliasing because
|
||||
scenarios round-trip through JSON and a single source of literal
|
||||
strings keeps the schema robust against renames in either domain.
|
||||
@@ -0,0 +1,100 @@
|
||||
# `types/visualizer.types` — visualizer type contracts
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared type definitions for the `NetworkVisualizer` subsystem and every
|
||||
consumer that produces or consumes a `VisualizerScene` IR. Centralising
|
||||
the contracts here means renderers, topology adapters, and stream
|
||||
adapters can be implemented independently without circular references
|
||||
into the implementation file. The types are runtime-free and tree-shake
|
||||
to nothing in consumer bundles.
|
||||
|
||||
## Features
|
||||
|
||||
- `VisualizerMode` — `"hierarchy" | "flow" | "heat"` literal union.
|
||||
- `VisualizerTier` — `"tier-0" | "tier-1" | "tier-2"` mirrors the canonical mesh tiers without taking a runtime dep on `network-plugin-topology`.
|
||||
- `NodeLayout` / `EdgeLayout` / `HeatCell` — primitive scene atoms.
|
||||
- `VisualizerFilter` — composable tier / latency-group / session / transport filters.
|
||||
- `VisualizerConfig` — `start({ mode, refreshRate, filter?, focus? })` input.
|
||||
- `TopologyAdapter` / `MessageStreamAdapter` — caller-supplied data sources.
|
||||
- `VisualizerScene` — the IR consumed by every `Renderer`.
|
||||
- `Renderer` / `RendererOutput` — pluggable byte-emitter contract.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { VisualizerScene } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
function describe(scene: VisualizerScene): string {
|
||||
return `${scene.mode}: ${scene.nodes.length} nodes, ${scene.edges.length} edges`;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
MessageStreamAdapter,
|
||||
Renderer,
|
||||
TopologyAdapter,
|
||||
} from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
class TopologyBridge implements TopologyAdapter {
|
||||
constructor(private readonly index: TopologyIndex) {}
|
||||
listNodes() {
|
||||
return this.index.nodes.map((n) => ({
|
||||
id: n.id,
|
||||
tier: n.tier,
|
||||
x: n.coords.x,
|
||||
y: n.coords.y,
|
||||
label: n.label,
|
||||
}));
|
||||
}
|
||||
listEdges() {
|
||||
return this.index.edges;
|
||||
}
|
||||
nodeById(id: string) {
|
||||
return this.listNodes().find((n) => n.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
const renderer: Renderer = {
|
||||
render(scene) {
|
||||
// produce caller-specific bytes (PNG, canvas, terminal, …)
|
||||
return {
|
||||
format: "png",
|
||||
bytes: encode(scene),
|
||||
mimeType: "image/png",
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type VisualizerMode = "hierarchy" | "flow" | "heat"`.
|
||||
- `type VisualizerTier = "tier-0" | "tier-1" | "tier-2"`.
|
||||
- `interface NodeLayout { id, tier, x, y, label }`.
|
||||
- `interface EdgeLayout { from, to, latencyMs, throughput, drops }`.
|
||||
- `interface HeatCell { id, intensity, latencyMs, saturation }`.
|
||||
- `interface VisualizerFilter { tiers?, latencyGroups?, sessions?, transports? }`.
|
||||
- `interface VisualizerConfig { mode, refreshRate, filter?, focus? }`.
|
||||
- `interface TopologyAdapter { listNodes(), listEdges(), nodeById(id) }`.
|
||||
- `interface MessageStreamAdapter { listFlows(), listHeat() }`.
|
||||
- `type VisualizerExportFormat = "svg" | "png"`.
|
||||
- `interface VisualizerScene { mode, nodes, edges, heat, width, height, focus? }`.
|
||||
- `interface RendererOutput { format, bytes, mimeType }`.
|
||||
- `interface Renderer { render(scene): RendererOutput }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The IR shape is the package's wire contract — every public renderer,
|
||||
test harness, snapshot generator, and downstream consumer reads it.
|
||||
Adding a field is backwards-compatible (consumers ignore unknown
|
||||
fields); removing or renaming a field is a major-version change. The
|
||||
intensity field on `HeatCell` is normalised 0..1 by convention but the
|
||||
SVG renderer clamps defensively, so producers that emit out-of-range
|
||||
values still render. Tier strings are mirrored from `network-plugin-topology`
|
||||
rather than re-imported because the visualizer should be usable in
|
||||
package boundaries that haven't installed the topology plugin (e.g. a
|
||||
pure-test consumer composing a fake topology by hand).
|
||||
@@ -0,0 +1,82 @@
|
||||
# `visualizer/network-visualizer` — `NetworkVisualizer`
|
||||
|
||||
## Purpose
|
||||
|
||||
Renders the live mesh as a `VisualizerScene` intermediate
|
||||
representation (IR) and delegates byte production to an injected
|
||||
`Renderer`. The IR captures three orthogonal views — the tier hierarchy,
|
||||
the live message-flow graph, and a per-node heat map of latency /
|
||||
saturation — and ships nodes, edges, and heat cells as plain data so any
|
||||
front-end (terminal, SVG, canvas, WebGL, Storybook) can consume the same
|
||||
output. The bundled `SvgRenderer` covers the common documentation /
|
||||
bug-report use case without pulling a heavyweight image library.
|
||||
|
||||
## Features
|
||||
|
||||
- Three modes: `hierarchy`, `flow`, `heat` — `start({ mode, refreshRate })` selects.
|
||||
- Filtering by tier, latency group, session, transport (Zod-validated).
|
||||
- Focus tracking — `focus(peerId)` / `clearFocus()`; non-existing peers throw `INSPECTOR_PATH_NOT_FOUND`.
|
||||
- `export(format)` — runs the injected renderer, validates `format` round-trip, surfaces failures as `VISUALIZER_RENDERER_FAILED`.
|
||||
- OpenTelemetry counters: `devtools.visualizer.render_total{mode}`, `devtools.visualizer.export_total{format}`.
|
||||
- Pluggable renderer via the `Renderer` interface — defaults to bundled SVG.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { NetworkVisualizer } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const viz = new NetworkVisualizer({ topology, stream });
|
||||
viz.start({ mode: "hierarchy", refreshRate: 30 });
|
||||
const scene = viz.render();
|
||||
console.log(scene.nodes.length, scene.edges.length);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkVisualizer,
|
||||
type Renderer,
|
||||
type VisualizerScene,
|
||||
} from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const pngRenderer: Renderer = {
|
||||
render(scene: VisualizerScene) {
|
||||
const bytes = encodePng(scene);
|
||||
return { format: "png", bytes, mimeType: "image/png" };
|
||||
},
|
||||
};
|
||||
|
||||
const viz = new NetworkVisualizer({
|
||||
topology,
|
||||
stream,
|
||||
renderer: pngRenderer,
|
||||
width: 1200,
|
||||
height: 900,
|
||||
});
|
||||
viz.start({
|
||||
mode: "heat",
|
||||
refreshRate: 15,
|
||||
filter: { tiers: ["tier-0", "tier-1"], sessions: ["lobby"] },
|
||||
});
|
||||
viz.focus("host-eu-1");
|
||||
const out = viz.export("png");
|
||||
await fs.writeFile("mesh.png", out.bytes);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class NetworkVisualizer` — constructor `(config: NetworkVisualizerConfig)`. `config.topology` and `config.stream` required; `config.renderer` defaults to bundled SVG; `config.width`/`config.height` default to 800×600.
|
||||
- `start(config: VisualizerConfig): void` — Zod-validates; throws `VISUALIZER_INVALID_MODE`.
|
||||
- `stop(): void` — clears active config.
|
||||
- `focus(peerId: string): void` — throws `INSPECTOR_PATH_NOT_FOUND` if unknown.
|
||||
- `clearFocus(): void` — resets focus.
|
||||
- `mode(): VisualizerMode` — current mode (throws `VISUALIZER_NOT_STARTED` if stopped).
|
||||
- `isRunning(): boolean`.
|
||||
- `render(): VisualizerScene` — emits IR; throws `VISUALIZER_EMPTY_TOPOLOGY` when filter eliminates all nodes.
|
||||
- `export(format: 'svg' | 'png'): RendererOutput` — runs renderer; throws `VISUALIZER_RENDERER_FAILED` on renderer failure or format mismatch.
|
||||
- `ALLOWED_VISUALIZER_MODES` — exported readonly tuple.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The IR is the contract — keeping the `VisualizerScene` shape stable means rendering technology can change without breaking consumers. PNG support is delivered via dependency injection rather than a hard `sharp`/`canvas` dep so universal consumers (Bun, browser, deno) can pick the encoder appropriate to their runtime. The `applyNodeFilter` step short-circuits when no `filter` is supplied so the hot rendering loop avoids per-node Set lookups in the common case. The `clearFocus()` method intentionally rebuilds the config object so `exactOptionalPropertyTypes` keeps the `focus` field absent rather than `undefined` — TypeScript's strictest mode treats those distinctly. The `INSPECTOR_PATH_NOT_FOUND` reuse for an unknown focus peer is deliberate: a peer id is a path-like identifier, and the inspector / visualizer share that vocabulary.
|
||||
@@ -0,0 +1,82 @@
|
||||
# `visualizer/svg-renderer` — bundled SVG `Renderer`
|
||||
|
||||
## Purpose
|
||||
|
||||
Pure-string SVG renderer that consumes a `VisualizerScene` and produces
|
||||
deterministic UTF-8 SVG bytes without touching the DOM, canvas, or any
|
||||
third-party encoding library. Used as the default renderer for
|
||||
`NetworkVisualizer`, the SVG output is suitable for documentation,
|
||||
PR attachments, regression test snapshots, and embedding in
|
||||
Storybook / MDX docs.
|
||||
|
||||
The renderer is intentionally tiny and dep-free so universal
|
||||
consumers (Bun, Deno, Node, browser) can bundle it without surprise. PNG
|
||||
output is delivered by injecting an encoder-aware renderer instead of
|
||||
shipping `sharp`/`canvas` here.
|
||||
|
||||
## Features
|
||||
|
||||
- `renderSvg(scene)` — pure function from `VisualizerScene` to `RendererOutput`.
|
||||
- `SvgRenderer` — `Renderer` adapter object suitable for direct injection.
|
||||
- Tier-coloured nodes (`tier-0` blue, `tier-1` green, `tier-2` orange).
|
||||
- Edge stroke-width scales with throughput; dropped frames flip stroke red.
|
||||
- Heat cells map a deterministic FNV-1a hash of the cell id to a 16×12 grid so the layout is stable across renders.
|
||||
- XML escaping on every string field so peer ids containing `<` / `&` cannot inject into the output.
|
||||
- Focus-aware opacity dimming on non-focused nodes.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { renderSvg } from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const out = renderSvg({
|
||||
mode: "hierarchy",
|
||||
nodes: [{ id: "a", tier: "tier-0", x: 100, y: 100, label: "Host" }],
|
||||
edges: [],
|
||||
heat: [],
|
||||
width: 400,
|
||||
height: 300,
|
||||
});
|
||||
console.log(new TextDecoder().decode(out.bytes));
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
NetworkVisualizer,
|
||||
SvgRenderer,
|
||||
} from "@suaveplan/network-plugin-devtools";
|
||||
|
||||
const viz = new NetworkVisualizer({
|
||||
topology,
|
||||
stream,
|
||||
renderer: SvgRenderer,
|
||||
width: 1024,
|
||||
height: 768,
|
||||
});
|
||||
viz.start({ mode: "flow", refreshRate: 10, focus: "host-1" });
|
||||
const svgBytes = viz.export("svg").bytes;
|
||||
await Bun.write("./flow.svg", svgBytes);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `renderSvg(scene: VisualizerScene): RendererOutput` — pure; never throws.
|
||||
- `SvgRenderer: Renderer` — adapter object; `SvgRenderer.render === renderSvg` call form.
|
||||
|
||||
`RendererOutput` carries `format: "svg"`, `bytes: Uint8Array`, and `mimeType: "image/svg+xml"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The renderer is deliberately deterministic — every numeric position is
|
||||
rendered via `toFixed(2)` so two semantically-identical scenes produce
|
||||
byte-identical SVG. This makes snapshot tests and visual diffs reliable.
|
||||
Heat-cell placement uses an FNV-1a hash mod a 16×12 grid; the grid size
|
||||
is hard-coded because the IR doesn't carry layout dimensions for cells
|
||||
(those belong to the heat layer, not the topology). Edges are skipped
|
||||
silently when either endpoint is missing from the scene's node list,
|
||||
which mirrors the visualizer's filter semantics — a tier-filtered scene
|
||||
must not crash because a partner node was dropped. XML escaping is
|
||||
applied to every consumer-supplied string (peer ids, labels) to prevent
|
||||
SVG-injection from a malicious or fuzz-generated topology.
|
||||
Reference in New Issue
Block a user