Accepted
2026-04-16
This document defines the performance benchmarking and regression tracking strategy for flowd.
The goals are:
- establish a reliable performance baseline (pre-ADR-0002)
- track performance across commits
- detect unintended regressions
- understand trade-offs introduced by architectural changes
Performance must be tracked continuously, not assumed.
This specification applies to:
- runtime execution
- message passing
- pipeline behavior
It explicitly includes:
- micro-benchmarks (isolated primitives)
- pipeline benchmarks (full runtime execution)
It excludes:
- production monitoring
- deep profiling
- micro-optimizations
All benchmarks MUST use:
Criterion.rs
Rationale:
- statistically sound measurements
- reproducibility
- noise reduction
Custom benchmarking implementations are NOT allowed.
Performance testing is divided into two mandatory layers:
Measure isolated building blocks without involving the runtime.
- ringbuffer throughput (push/pop)
- edge/message transfer cost
- cross-thread handover latency
Understand the cost of fundamental primitives in isolation.
- MUST NOT use the runtime
- MUST isolate the specific primitive under test
- MUST be minimal and deterministic
Measure full system behavior using the actual flowd runtime.
Pipeline benchmarks MUST execute through the real flowd runtime.
This includes:
- real component execution
- real edges
- real pipeline construction
- actual runtime behavior
- bypassing the runtime
- manually wiring functions instead of using flowd
- simulating pipelines outside the runtime
- replacing runtime execution with shortcuts
If the runtime is bypassed:
- results are invalid
- overhead (edges, scheduling) is invisible
- benchmarks become misleading
Pipeline benchmarks MUST reuse existing flowd components whenever possible.
- Source components
- Demux / fan-out components
- Merge / sink components
If suitable components exist:
They MUST be used.
If suitable components do NOT exist:
Minimal local components MAY be implemented inside
/benches
Constraints:
- strictly minimal logic
- only for benchmarking
- clearly documented as benchmark-only
- ensures realistic execution paths
- avoids artificial benchmarks
- keeps tests aligned with real usage
Source → Node → Node → Sink
Purpose:
- baseline throughput
- minimal overhead measurement
Source → Node → (Node A, Node B, Node C) → Merge → Sink
Purpose:
- message distribution cost
- allocation / duplication overhead
Source A →
→ Merge → Node → Sink
Source B →
Purpose:
- coordination overhead
- large message count (e.g. 1M+)
Purpose:
- detect long-run degradation
- observe stability
- artificial delay (sleep)
Purpose:
- baseline for future async/scheduler changes
Each benchmark MUST measure:
messages per second
end-to-end processing time
Handled by Criterion:
- variance
- confidence intervals
- allocation behavior
- memory trends
The system MUST NOT use:
baseline.json- hardcoded performance thresholds
Performance is tracked via:
- benchmark execution per commit
- historical comparison
- CI artifact storage
Results SHOULD be:
- stored as Criterion outputs
- optionally exported (CSV/JSON)
- tracked via CI and external tools
Performance is evaluated over time using:
Bencher
- track performance across commits
- compare trends
- identify regressions manually (initially)
Not all performance changes are regressions.
A regression is:
- a significant degradation
- without architectural justification
(e.g. scheduler, backpressure)
- run full benchmark suite
- document baseline
- rerun benchmarks
- compare results
- document impact
Benchmarks SHOULD:
- run on stable hardware
- avoid background noise
- be reproducible
This system does NOT aim to:
- guarantee absolute performance numbers
- replace profiling tools
- simulate full production load
After ADR-0002 (Scheduler):
- measure fairness vs throughput trade-offs
After ADR-0008 (Memory Model):
- compare Vec vs Arc vs zero-copy
After ADR-0009 (Distribution):
- add network-level benchmarks
Measure primitives in isolation. Measure behavior in composition.
Benchmarks must reflect real system behavior — not idealized shortcuts.
This specification ensures:
- reliable baseline measurement
- continuous performance tracking
- separation of cause (micro) and effect (pipeline)
- realistic, runtime-based benchmarking
The current implementation in this repository uses:
- Criterion benchmark suites at:
benches/micro_benchmarks.rsbenches/pipeline_benchmarks.rs
- benchmark harness configuration in
Cargo.toml:[[bench]]name = "micro_benchmarks"harness = false[[bench]]name = "pipeline_benchmarks"harness = false
- GitHub Actions workflow:
.github/workflows/performance-benchmarks.yml
Implemented micro-bench scenarios (primitive layer):
- ringbuffer push/pop throughput
- edge/message transfer cost (payload-size dependent)
- cross-thread handover latency
Implemented pipeline scenarios (runtime layer):
- linear pipeline (
Source → Node → Node → Sink) - fan-out pipeline (
Source → Demux → Node A/B/C → Merge → Sink) - fan-in / merge pipeline (
Source A/B → Merge → Node → Sink) - high-volume linear pipeline
- IO-simulated pipeline (
Source → Delay → Node → Sink)
Each pipeline scenario has two benchmark sets:
- persistence-loaded graph set:
- graph is serialized and loaded with the same
read_to_string + serde_json::from_strmechanism as runtime startup
- graph is serialized and loaded with the same
- direct graph/runtime set:
- graph and runtime structs are synthesized in-memory and runtime functions are called directly (without websocket graph mutation)
Runtime execution path used by pipeline benchmarks:
- shared internal
run_graph()insrc/lib.rs network:starthandler and benchmark harness both callrun_graph()- component instantiation, edge wiring, and execution loop are therefore exercised through the same runtime start path
- runtime fix included:
- source array-port validation now checks repeated use of the same source port (not only process), enabling valid multi-outport fan-out graphs
Metrics currently captured:
- throughput via Criterion
Throughput::Elements(messages/sec) in both micro and pipeline groups - latency via dedicated Criterion latency groups (including end-to-end runtime scenarios)
- statistical stability via Criterion defaults + configured sample sizes
Run full benchmark suite:
cargo bench --bench micro_benchmarks --bench pipeline_benchmarks -- --noplotOutputs are stored in:
target/criterion/
On each push and pull_request, CI:
- runs both Criterion benchmark targets (
micro_benchmarksandpipeline_benchmarks) - uploads
target/criterionas build artifact - optionally uploads results to Bencher (if configured)
No static baseline file is used.
To enable commit-level history tracking in CI:
- Create a Bencher project and copy its project slug.
- Add repository secret
BENCHER_API_TOKEN. - Add repository variable
BENCHER_PROJECT(project slug). - Push a commit or open a PR to trigger the workflow.
CI then publishes each benchmark run with:
- adapter:
rust_criterion - branch metadata from GitHub event context
- PR start-point comparison for regression review
Use trend comparison between commits, not single-run absolutes:
- throughput drops indicate potential regression in data movement
- latency increases indicate added pipeline overhead
- high-volume drift indicates long-run behavior changes
Manual regression review is the default for now. Soft threshold automation can be added later.