- omnivox::server::<T>()/client::<T>() builders; plain closures, enum in, match in the callback — the original C++ UX with the ceremony removed - serde + bincode replace the hand-rolled Cereal codec (vectors.rs still governs the crypto leaves via the C++ golden oracles) - v2.1 frames: u32 len | type | ct | tag; nonce derived from the sequence counter (no wire nonce), type byte authenticated as AAD; any frame-level failure is fatal, authenticated-but-undecodable payloads are skipped - stream lane: send_stream/on_stream, 1 MiB frames, shared sequence with messages (exact sender interleaving preserved); see dump-agent/viewer - per-connection bounded queues + writer tasks; reply/reply_stream are non-blocking try_send lanes for callbacks - benches: seal/open criterion group + transport harness (spray msg/s, stream GiB/s, raw loopback baseline) - profile.dev.package scrypt/pbkdf2 opt-level 3: golden-vector scrypt at N=2^17 was unusable in debug builds - PROTOCOL-V2.md and README rewritten for v2.1
227 lines
7.3 KiB
Rust
227 lines
7.3 KiB
Rust
//! Throughput benchmarks for OmniVox v2.1, answering: what does the
|
|
//! protocol cost versus raw loopback TCP on this machine?
|
|
//!
|
|
//! - `crypto`: criterion stats for frame seal/open at three sizes.
|
|
//! - `transport`: hand-rolled async harness — spray (small messages/s),
|
|
//! dump (GiB/s at 1 MiB stream frames), and a raw-TCP echo baseline.
|
|
//!
|
|
//! Run: `cargo bench --bench transport`
|
|
|
|
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group};
|
|
use omnivox::{Config, FrameType, frame::seal_into};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
sync::{
|
|
Arc,
|
|
atomic::{AtomicU64, Ordering},
|
|
},
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct M(#[allow(dead_code)] Vec<u8>);
|
|
|
|
fn crypto_benches(c: &mut Criterion) {
|
|
let mut g = c.benchmark_group("crypto");
|
|
let key = [1u8; 32];
|
|
for size in [256usize, 4096, 1 << 20] {
|
|
let payload = vec![7u8; size];
|
|
g.throughput(Throughput::Bytes(size as u64));
|
|
g.bench_function(BenchmarkId::new("seal", size), |b| {
|
|
let mut buf = Vec::with_capacity(size + 64);
|
|
b.iter(|| {
|
|
seal_into(
|
|
&key,
|
|
1,
|
|
FrameType::Message,
|
|
std::hint::black_box(&payload),
|
|
&mut buf,
|
|
)
|
|
});
|
|
});
|
|
// open measured as copy + decrypt-in-place of a sealed frame
|
|
let sealed = omnivox::frame::seal(&key, 1, FrameType::Message, &payload);
|
|
g.bench_function(BenchmarkId::new("open", size), |b| {
|
|
b.iter(|| {
|
|
let mut body = std::hint::black_box(&sealed[4..]).to_vec();
|
|
let (kind, _pt) = omnivox::frame::open_in_place(&key, 1, &mut body).unwrap();
|
|
std::hint::black_box(kind)
|
|
});
|
|
});
|
|
}
|
|
g.finish();
|
|
}
|
|
|
|
fn test_config(stream_frame: u32) -> Config {
|
|
Config {
|
|
idle_timeout_ms: 60_000,
|
|
body_stall_ms: 60_000,
|
|
send_timeout_ms: 60_000,
|
|
max_message_size: 2 * 1024 * 1024,
|
|
max_stream_frame: stream_frame,
|
|
allow_insecure_defaults: true,
|
|
..Config::default()
|
|
}
|
|
}
|
|
|
|
fn transport_harness() {
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
rt.block_on(async {
|
|
// ---- spray: small messages per second ----
|
|
const SPRAY_PORT: u16 = 18701;
|
|
let rx = Arc::new(AtomicU64::new(0));
|
|
let r = Arc::clone(&rx);
|
|
let server = omnivox::server::<M>()
|
|
.key([7u8; 32])
|
|
.config(test_config(1 << 20))
|
|
.on_data(move |_: &M, _| {
|
|
r.fetch_add(1, Ordering::Relaxed);
|
|
})
|
|
.serve(SPRAY_PORT)
|
|
.await
|
|
.unwrap();
|
|
let client = omnivox::client::<M>()
|
|
.key([7u8; 32])
|
|
.config(test_config(1 << 20))
|
|
.on_data(|_: &M, _| {})
|
|
.connect("127.0.0.1", SPRAY_PORT)
|
|
.await
|
|
.unwrap();
|
|
|
|
let msg = M(vec![9u8; 256]);
|
|
let n: u64 = 200_000;
|
|
let start = Instant::now();
|
|
let mut sent = 0;
|
|
while sent < n {
|
|
if client.send(&msg).await {
|
|
sent += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
// drain
|
|
while rx.load(Ordering::Relaxed) < sent {
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
}
|
|
let secs = start.elapsed().as_secs_f64();
|
|
println!(
|
|
"spray: {:>10.0} msg/s ({} x 256 B, encrypted round trip loopback)",
|
|
sent as f64 / secs,
|
|
sent
|
|
);
|
|
client.stop().await;
|
|
server.stop().await;
|
|
|
|
// ---- dump: stream GiB/s ----
|
|
const DUMP_PORT: u16 = 18702;
|
|
let rx = Arc::new(AtomicU64::new(0));
|
|
let r = Arc::clone(&rx);
|
|
let server = omnivox::server::<M>()
|
|
.key([7u8; 32])
|
|
.config(test_config(1 << 20))
|
|
.on_stream(move |b, _| {
|
|
r.fetch_add(b.len() as u64, Ordering::Relaxed);
|
|
})
|
|
.serve(DUMP_PORT)
|
|
.await
|
|
.unwrap();
|
|
let client = omnivox::client::<M>()
|
|
.key([7u8; 32])
|
|
.config(test_config(1 << 20))
|
|
.on_data(|_: &M, _| {})
|
|
.connect("127.0.0.1", DUMP_PORT)
|
|
.await
|
|
.unwrap();
|
|
|
|
let page = vec![3u8; 1 << 20]; // 1 MiB
|
|
let pages: u64 = 256; // 256 MiB
|
|
let start = Instant::now();
|
|
for _ in 0..pages {
|
|
client.send_stream(&page).await;
|
|
}
|
|
let want = pages << 20;
|
|
while rx.load(Ordering::Relaxed) < want {
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
}
|
|
let secs = start.elapsed().as_secs_f64();
|
|
println!(
|
|
"dump: {:>10.2} GiB/s ({} MiB over encrypted stream lane, loopback)",
|
|
want as f64 / secs / (1u64 << 30) as f64,
|
|
want >> 20
|
|
);
|
|
client.stop().await;
|
|
server.stop().await;
|
|
|
|
// ---- raw loopback baseline (no protocol) ----
|
|
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 18703))
|
|
.await
|
|
.unwrap();
|
|
let echo = tokio::spawn(async move {
|
|
let (s, _) = listener.accept().await.unwrap();
|
|
let (mut r, mut w) = s.into_split();
|
|
let mut buf = vec![0u8; 1 << 20];
|
|
loop {
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
if r.read_exact(&mut buf).await.is_err() {
|
|
break;
|
|
}
|
|
if w.write_all(&buf).await.is_err() {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
let raw = tokio::net::TcpStream::connect(("127.0.0.1", 18703))
|
|
.await
|
|
.unwrap();
|
|
raw.set_nodelay(true).unwrap();
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
let (mut rr, mut rw) = raw.into_split();
|
|
let chunk: Vec<u8> = vec![5u8; 1 << 20];
|
|
let total: u64 = 64 << 20;
|
|
let reader = tokio::spawn(async move {
|
|
let mut got = 0u64;
|
|
let mut sink = vec![0u8; 1 << 20];
|
|
while got < total {
|
|
let n = rr.read(&mut sink).await.unwrap();
|
|
if n == 0 {
|
|
break;
|
|
}
|
|
got += n as u64;
|
|
}
|
|
got
|
|
});
|
|
let start = Instant::now();
|
|
let mut written = 0u64;
|
|
while written < total {
|
|
rw.write_all(&chunk).await.unwrap();
|
|
written += chunk.len() as u64;
|
|
}
|
|
let want = total;
|
|
while !reader.is_finished() {
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
let got = reader.await.unwrap();
|
|
assert!(got >= want, "raw echo short: {got}");
|
|
let secs = start.elapsed().as_secs_f64();
|
|
println!(
|
|
"raw baseline: {:>10.2} GiB/s (unencrypted loopback write+echo, same box)",
|
|
want as f64 / secs / (1u64 << 30) as f64
|
|
);
|
|
echo.abort();
|
|
});
|
|
}
|
|
|
|
criterion_group! {
|
|
name = crypto;
|
|
config = Criterion::default().sample_size(20).measurement_time(Duration::from_secs(2));
|
|
targets = crypto_benches
|
|
}
|
|
|
|
fn main() {
|
|
crypto();
|
|
transport_harness();
|
|
}
|