3ebcb3f758
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.
146 lines
4.1 KiB
Go
146 lines
4.1 KiB
Go
// Throughput benchmark for the Wickra Go bindings.
|
|
//
|
|
// Measures how many indicator updates per second the cgo binding sustains,
|
|
// both per-tick (streaming Update) and bulk (Batch), over a synthetic OHLCV
|
|
// series. It is the Go counterpart of the Node throughput.js and the Rust
|
|
// criterion benches: it benchmarks Wickra's own O(1) streaming engine across
|
|
// the Go<->C-ABI boundary (there is no comparable streaming TA library to
|
|
// compare against), so the headline number is raw per-binding throughput /
|
|
// FFI overhead, not a cross-library ratio.
|
|
//
|
|
// Three indicators are timed, chosen by FFI call-signature archetype rather
|
|
// than algorithm: SMA (1-in -> 1-out), ATR (multi-in -> 1-out), and MACD
|
|
// (1-in -> multi-out). Streaming is timed for all three; batch only for the
|
|
// single-output SMA and ATR (multi-output batch is not exposed uniformly).
|
|
//
|
|
// Provision the C ABI library first (see bindings/go/README.md), then run:
|
|
//
|
|
// cd bindings/go/benchmarks
|
|
// go run . # 200k bars (default)
|
|
// go run . -bars 1000000
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"time"
|
|
|
|
wickra "github.com/wickra-lib/wickra/bindings/go"
|
|
)
|
|
|
|
func main() {
|
|
bars := flag.Int("bars", 200_000, "number of synthetic bars to feed")
|
|
flag.Parse()
|
|
n := *bars
|
|
if n < 1000 {
|
|
fmt.Println("-bars must be >= 1000")
|
|
return
|
|
}
|
|
|
|
// Deterministic synthetic OHLCV (no RNG, so runs are comparable).
|
|
open := make([]float64, n)
|
|
high := make([]float64, n)
|
|
low := make([]float64, n)
|
|
closeP := make([]float64, n)
|
|
volume := make([]float64, n)
|
|
timestamp := make([]int64, n)
|
|
for i := 0; i < n; i++ {
|
|
mid := 100 + math.Sin(float64(i)*0.001)*20 + float64(i)*1e-4
|
|
c := mid + math.Sin(float64(i)*0.05)*2
|
|
closeP[i] = c
|
|
open[i] = mid
|
|
high[i] = math.Max(c, mid) + 1.5
|
|
low[i] = math.Min(c, mid) - 1.5
|
|
volume[i] = 1000 + float64(i%97)*13
|
|
timestamp[i] = int64(i)
|
|
}
|
|
|
|
mups := func(d time.Duration) float64 {
|
|
return float64(n) / d.Seconds() / 1e6
|
|
}
|
|
|
|
// Median elapsed over a few repetitions, after one warmup pass.
|
|
timeFn := func(fn func()) time.Duration {
|
|
fn() // warmup
|
|
const reps = 3
|
|
samples := make([]time.Duration, reps)
|
|
for r := 0; r < reps; r++ {
|
|
t0 := time.Now()
|
|
fn()
|
|
samples[r] = time.Since(t0)
|
|
}
|
|
sort.Slice(samples, func(a, b int) bool { return samples[a] < samples[b] })
|
|
return samples[reps/2]
|
|
}
|
|
|
|
type indicator struct {
|
|
name string
|
|
stream func()
|
|
batch func() // nil -> streaming only
|
|
}
|
|
|
|
indicators := []indicator{
|
|
{
|
|
name: "SMA(20)",
|
|
stream: func() {
|
|
ind, _ := wickra.NewSma(20)
|
|
for i := 0; i < n; i++ {
|
|
ind.Update(closeP[i])
|
|
}
|
|
ind.Close()
|
|
},
|
|
batch: func() {
|
|
ind, _ := wickra.NewSma(20)
|
|
ind.Batch(closeP)
|
|
ind.Close()
|
|
},
|
|
},
|
|
{
|
|
name: "ATR(14)",
|
|
stream: func() {
|
|
ind, _ := wickra.NewAtr(14)
|
|
for i := 0; i < n; i++ {
|
|
ind.Update(open[i], high[i], low[i], closeP[i], volume[i], timestamp[i])
|
|
}
|
|
ind.Close()
|
|
},
|
|
batch: func() {
|
|
ind, _ := wickra.NewAtr(14)
|
|
ind.Batch(open, high, low, closeP, volume, timestamp)
|
|
ind.Close()
|
|
},
|
|
},
|
|
{
|
|
name: "MACD(12,26,9)",
|
|
stream: func() {
|
|
ind, _ := wickra.NewMacdIndicator(12, 26, 9)
|
|
for i := 0; i < n; i++ {
|
|
ind.Update(closeP[i])
|
|
}
|
|
ind.Close()
|
|
},
|
|
batch: nil, // multi-output: streaming only
|
|
},
|
|
}
|
|
|
|
fmt.Printf("Wickra Go throughput - %d bars (median of 3 runs)\n\n", n)
|
|
fmt.Printf("%-22s%20s%18s\n", "Indicator", "streaming (Mupd/s)", "batch (Mupd/s)")
|
|
fmt.Println("------------------------------------------------------------")
|
|
|
|
for _, ind := range indicators {
|
|
streamMups := fmt.Sprintf("%.1f", mups(timeFn(ind.stream)))
|
|
batchMups := "-"
|
|
if ind.batch != nil {
|
|
batchMups = fmt.Sprintf("%.1f", mups(timeFn(ind.batch)))
|
|
}
|
|
fmt.Printf("%-22s%20s%18s\n", ind.name, streamMups, batchMups)
|
|
}
|
|
|
|
fmt.Print("\nMupd/s = million indicator updates per second. Streaming is the per-tick\n",
|
|
"Update path crossing the Go<->C-ABI boundary once per value; batch is the\n",
|
|
"bulk slice path (one boundary crossing). Higher is better. Numbers are\n",
|
|
"machine-dependent - use them for relative comparison, not as a speed claim.\n")
|
|
}
|