Files
wickra/examples/rust/src/bin/throughput.rs
T
kingchenc 3ebcb3f758 Per-binding throughput benchmarks + test-coverage gaps (#246)
Adds a `throughput` benchmark to every target and closes two small
test-coverage documentation/QA gaps. One PR, no merge of binding code beyond
the additive benchmarks and one C test.

## 1. Per-binding throughput benchmarks (all 9 targets)

Each benchmark feeds a deterministic synthetic OHLCV series through three
indicators chosen by **FFI call-signature archetype** (not algorithm — the same
Rust core runs underneath all bindings):

- `SMA(20)` — 1-in → 1-out (baseline boundary cost)
- `ATR(14)` — multi-in → 1-out (input marshalling)
- `MACD(12,26,9)` — 1-in → multi-out (output marshalling)

Streaming is timed for all three; batch for the single-output SMA and ATR
(median of 3 runs, after a warmup pass).

New: Python (PyO3), WASM, C (CMake), C# (Stopwatch), Go, Java (FFM), R, and the
Rust core baseline (`examples/rust/.../throughput.rs`, **no FFI** — the ceiling
the bindings are measured against and the value their batch paths converge
towards). Node already had `throughput.js`.

**Not a speed claim:** there is no comparable streaming TA library for C, C#,
Go, Java, R or WASM to compare against, so these are raw per-binding throughput
numbers documenting each language's FFI overhead — see BENCHMARKS.md §3. The
"Wickra is fast" claim still lives in §1/§2 (Rust core + the Python/Rust
cross-library runs).

## 2. README `## Testing`: C# and C bullets

The section listed every layer except C# and C, even though both have suites.
Adds the two missing bullets.

## 3. C archetype ctest

`examples/c/archetypes.c` drives one indicator per FFI archetype through the
real C boundary (scalar + batch==streaming, multi-output, bars, profile, array
input) plus reset, invalid-parameter and NULL-safety — the C counterpart of the
Go/R/Java archetype suites. Runs on three OSes via the existing CMake/ctest.

## Notes

- Benchmarks are not CI-gated (manual-run scripts, like the existing
  `throughput.js`); no `ci.yml`/`release.yml` changes.
- Docs: BENCHMARKS.md §3, a `## Benchmark` section in every binding README, a
  CHANGELOG entry.
- Verified locally by running: Rust, Python, C, C#, Go, Java (real numbers); the
  C archetype ctest with `-Wall -Wextra -Wpedantic -Werror`. WASM and R are
  API-correct and syntax-checked but need their own toolchains to run.
2026-06-10 03:46:38 +02:00

132 lines
4.2 KiB
Rust

//! Throughput benchmark for the Wickra Rust core — the zero-FFI baseline.
//!
//! Reports streaming (`update`) and batch updates-per-second over a synthetic
//! OHLCV series, in the same format as every binding's `throughput` benchmark.
//! Rust has no FFI boundary — it calls the core directly — so these numbers are
//! the ceiling the per-binding benchmarks are measured against, and the value
//! their `batch` paths converge towards. See the repository BENCHMARKS.md §3.
//!
//! For per-update latency and the cross-library comparison, use the criterion
//! harnesses instead: `cargo bench -p wickra` and `cargo bench -p wickra-bench`.
//!
//! Run:
//! cargo run -p wickra-examples --release --bin throughput # 200k bars
//! cargo run -p wickra-examples --release --bin throughput -- 1000000
use std::time::Instant;
use wickra::{Atr, Candle, Indicator, MacdIndicator, Sma};
/// Median elapsed-ns over a few repetitions, after one warmup pass.
fn time_ns(mut run: impl FnMut()) -> u128 {
run(); // warmup
let mut samples = [0u128; 3];
for sample in &mut samples {
let start = Instant::now();
run();
*sample = start.elapsed().as_nanos();
}
samples.sort_unstable();
samples[1]
}
fn main() {
let bars: usize = std::env::args()
.nth(1)
.and_then(|arg| arg.parse().ok())
.filter(|&n| n >= 1000)
.unwrap_or(200_000);
// Deterministic synthetic OHLCV (no RNG, so runs are comparable).
let mut open = Vec::with_capacity(bars);
let mut high = Vec::with_capacity(bars);
let mut low = Vec::with_capacity(bars);
let mut close = Vec::with_capacity(bars);
let mut volume = Vec::with_capacity(bars);
for i in 0..bars {
let mid = 100.0 + (i as f64 * 0.001).sin() * 20.0 + i as f64 * 1e-4;
let c = mid + (i as f64 * 0.05).sin() * 2.0;
close.push(c);
open.push(mid);
high.push(c.max(mid) + 1.5);
low.push(c.min(mid) - 1.5);
volume.push(1000.0 + (i % 97) as f64 * 13.0);
}
// ATR streams a Candle per tick; build them once, outside the timed loop.
let candles: Vec<Candle> = (0..bars)
.map(|i| {
Candle::new(
open[i],
high[i],
low[i],
close[i],
volume[i],
i64::try_from(i).unwrap(),
)
.unwrap()
})
.collect();
let mups = |ns: u128| bars as f64 / (ns as f64 / 1e9) / 1e6;
// SMA (scalar 1-in/1-out), ATR (multi-in/1-out), MACD (1-in/multi-out).
let sma_stream = time_ns(|| {
let mut ind = Sma::new(20).unwrap();
for &price in &close {
ind.update(price);
}
});
let sma_batch = time_ns(|| {
let mut ind = Sma::new(20).unwrap();
ind.batch_nan(&close);
});
let atr_stream = time_ns(|| {
let mut ind = Atr::new(14).unwrap();
for &candle in &candles {
ind.update(candle);
}
});
let atr_batch = time_ns(|| {
let mut ind = Atr::new(14).unwrap();
ind.batch_atr(&high, &low, &close);
});
let macd_stream = time_ns(|| {
let mut ind = MacdIndicator::new(12, 26, 9).unwrap();
for &price in &close {
ind.update(price);
}
});
println!("Wickra Rust core throughput - {bars} bars (median of 3 runs)\n");
println!(
"{:<22}{:>20}{:>18}",
"Indicator", "streaming (Mupd/s)", "batch (Mupd/s)"
);
println!("{}", "-".repeat(60));
println!(
"{:<22}{:>20.1}{:>18.1}",
"SMA(20)",
mups(sma_stream),
mups(sma_batch)
);
println!(
"{:<22}{:>20.1}{:>18.1}",
"ATR(14)",
mups(atr_stream),
mups(atr_batch)
);
println!(
"{:<22}{:>20.1}{:>18}",
"MACD(12,26,9)",
mups(macd_stream),
"-"
);
println!(
"\nMupd/s = million indicator updates per second. This is the Rust core with\n\
no FFI boundary, so it is the ceiling for the per-binding benchmarks and\n\
the value their batch paths converge towards. Numbers are machine-dependent\n\
- use them for relative comparison, not as a speed claim."
);
}