From 1f70f99ba628598884b049d9a0b62ae034a3a5a4 Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:45:23 -0600 Subject: [PATCH] Add protocol benchmark example Measure representative protocol paths in nanoseconds over hundreds of samples and report standard deviation so framing, routing, and local-delivery changes can be compared with less jitter. --- examples/protocol_bench.rs | 264 +++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 examples/protocol_bench.rs diff --git a/examples/protocol_bench.rs b/examples/protocol_bench.rs new file mode 100644 index 0000000..f41d913 --- /dev/null +++ b/examples/protocol_bench.rs @@ -0,0 +1,264 @@ +use std::hint::black_box; +use std::time::Instant; + +use unshell::protocol::tree::{ChildRoute, Endpoint, Ingress, LeafSpec, LocalEvent, ProtocolEndpoint}; +use unshell::protocol::{CallMessage, PacketHeader, PacketType, decode_frame, encode_packet}; + +const SAMPLES: usize = 500; +const ITERS: usize = 1_000; + +fn main() { + println!("protocol benchmark"); + println!("samples: {SAMPLES}"); + println!("iterations/sample: {ITERS}"); + println!(); + + let benches = [ + bench_encode_call(), + bench_decode_call(), + bench_forward_call_receive(), + bench_local_call_receive(), + bench_hook_data_receive(), + ]; + + println!("{:32} {:>14} {:>14} {:>14}", "benchmark", "mean ns/op", "stddev", "samples"); + for bench in benches { + println!( + "{:32} {:>14.2} {:>14.2} {:>14}", + bench.name, bench.mean_ns, bench.stddev_ns, bench.samples + ); + } +} + +struct BenchResult { + name: &'static str, + mean_ns: f64, + stddev_ns: f64, + samples: usize, +} + +fn bench_encode_call() -> BenchResult { + let header = PacketHeader { + packet_type: PacketType::Call, + src_path: path(&["root"]), + dst_path: path(&["root", "worker"]), + dst_leaf: Some(String::from("service")), + hook_id: None, + }; + let message = CallMessage { + procedure_id: String::from("example.service.v1.invoke"), + data: vec![7; 64], + response_hook: None, + }; + + run_bench("encode_call", || { + let frame = encode_packet(black_box(&header), black_box(&message)).expect("encode should work"); + black_box(frame.len()); + }) +} + +fn bench_decode_call() -> BenchResult { + let header = PacketHeader { + packet_type: PacketType::Call, + src_path: path(&["root"]), + dst_path: path(&["root", "worker"]), + dst_leaf: Some(String::from("service")), + hook_id: None, + }; + let message = CallMessage { + procedure_id: String::from("example.service.v1.invoke"), + data: vec![9; 64], + response_hook: None, + }; + let frame = encode_packet(&header, &message).expect("seed frame should encode"); + + run_bench("decode_call", || { + let parsed = decode_frame(black_box(frame.as_slice())).expect("decode should work"); + let call = parsed.deserialize_call().expect("call should deserialize"); + black_box(call.data.len()); + }) +} + +fn bench_forward_call_receive() -> BenchResult { + run_prebuilt_bench("forward_call_receive", build_forward_call_cases, |(mut root, frame)| { + let outcome = root.receive(&Ingress::Local, frame).expect("forward receive should work"); + black_box(outcome.forward.is_some()); + }) +} + +fn bench_local_call_receive() -> BenchResult { + run_prebuilt_bench("local_call_receive", build_local_call_cases, |(mut endpoint, frame)| { + let outcome = endpoint.receive(&Ingress::Parent, frame).expect("local call should work"); + match black_box(outcome.event) { + Some(LocalEvent::Call { .. }) => {} + other => panic!("expected local call event, got {other:?}"), + } + }) +} + +fn bench_hook_data_receive() -> BenchResult { + run_prebuilt_bench("hook_data_receive", build_hook_data_cases, |(mut host, frame)| { + let outcome = host + .receive(&Ingress::Child(path(&["worker"])), frame) + .expect("hook data should work"); + match black_box(outcome.event) { + Some(LocalEvent::Data { .. }) => {} + other => panic!("expected local data event, got {other:?}"), + } + }) +} + +fn run_bench(name: &'static str, mut op: impl FnMut()) -> BenchResult { + let mut samples = Vec::with_capacity(SAMPLES); + for _ in 0..SAMPLES { + let start = Instant::now(); + for _ in 0..ITERS { + op(); + } + let elapsed = start.elapsed().as_nanos() as f64 / ITERS as f64; + samples.push(elapsed); + } + summarize(name, &samples) +} + +fn run_prebuilt_bench( + name: &'static str, + mut build_cases: F, + mut op: impl FnMut(T), +) -> BenchResult +where + F: FnMut() -> Vec, +{ + let mut repeated = Vec::with_capacity(SAMPLES); + for _ in 0..SAMPLES { + let mut cases = build_cases(); + assert_eq!(cases.len(), ITERS); + let start = Instant::now(); + for case in cases.drain(..) { + op(case); + } + let elapsed = start.elapsed().as_nanos() as f64 / ITERS as f64; + repeated.push(elapsed); + } + summarize(name, &repeated) +} + +fn build_forward_call_cases() -> Vec<(ProtocolEndpoint, unshell::protocol::FrameBytes)> { + (0..ITERS) + .map(|_| { + let mut root = ProtocolEndpoint::new( + Vec::new(), + None, + vec![ChildRoute::registered(path(&["edge"]))], + Vec::new(), + ); + let hook_id = root.allocate_hook_id(); + let frame = root + .make_call( + path(&["edge", "worker"]), + Some(String::from("service")), + String::from("example.service.v1.invoke"), + Some(hook_id), + vec![1; 32], + ) + .expect("seed call should encode"); + (root, frame) + }) + .collect() +} + +fn build_local_call_cases() -> Vec<(ProtocolEndpoint, unshell::protocol::FrameBytes)> { + (0..ITERS) + .map(|_| { + let endpoint = ProtocolEndpoint::new( + path(&["worker"]), + Some(Vec::new()), + Vec::new(), + vec![LeafSpec { + name: String::from("service"), + procedures: vec![String::from("example.service.v1.invoke")], + }], + ); + let frame = encode_packet( + &PacketHeader { + packet_type: PacketType::Call, + src_path: Vec::new(), + dst_path: path(&["worker"]), + dst_leaf: Some(String::from("service")), + hook_id: None, + }, + &CallMessage { + procedure_id: String::from("example.service.v1.invoke"), + data: vec![2; 32], + response_hook: Some(unshell::protocol::HookTarget { + hook_id: 42, + return_path: Vec::new(), + }), + }, + ) + .expect("seed local call should encode"); + (endpoint, frame) + }) + .collect() +} + +fn build_hook_data_cases() -> Vec<(ProtocolEndpoint, unshell::protocol::FrameBytes)> { + (0..ITERS) + .map(|_| { + let mut host = ProtocolEndpoint::new( + Vec::new(), + None, + vec![ChildRoute::registered(path(&["worker"]))], + Vec::new(), + ); + let hook_id = host.allocate_hook_id(); + host.make_call( + path(&["worker"]), + None, + String::from("example.service.v1.invoke"), + Some(hook_id), + vec![3; 8], + ) + .expect("seed active hook should encode"); + let frame = encode_packet( + &PacketHeader { + packet_type: PacketType::Data, + src_path: path(&["worker"]), + dst_path: Vec::new(), + dst_leaf: None, + hook_id: Some(hook_id), + }, + &unshell::protocol::DataMessage { + procedure_id: String::from("example.service.v1.invoke"), + data: vec![4; 16], + end_hook: false, + }, + ) + .expect("seed data should encode"); + (host, frame) + }) + .collect() +} + +fn summarize(name: &'static str, samples: &[f64]) -> BenchResult { + let mean = samples.iter().sum::() / samples.len() as f64; + let variance = samples + .iter() + .map(|sample| { + let delta = sample - mean; + delta * delta + }) + .sum::() + / samples.len() as f64; + + BenchResult { + name, + mean_ns: mean, + stddev_ns: variance.sqrt(), + samples: samples.len(), + } +} + +fn path(parts: &[&str]) -> Vec { + parts.iter().map(|part| String::from(*part)).collect() +}