diff --git a/Cargo.toml b/Cargo.toml index 4f0b7fcc7e..c3b4f98384 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,11 +23,15 @@ futures = "0.3.30" clap = { version = "4.5.4", features = ["derive"] } serde = { version = "1.0", features = ["derive"], optional = true } serde_bytes = "0.11" +arbitrary = { version = "1", features = ["derive"], optional = true } [dev-dependencies] tracing-test = "0.2.4" tracing-subscriber = "0.3.19" udp-stream = "0.0.12" +arbitrary = { version = "1.4.1", features = ["derive"] } +criterion = { version = "0.5.1", features = ["html_reports", "async_tokio"] } +rand = "0.9.1" [build-dependencies] convert_case = "0.6.0" @@ -35,7 +39,17 @@ quote = "1.0" proc-macro2 = "1.0.24" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.64" +arbitrary = { version = "1.4.1", features = ["derive"] } [features] local_runner = [] +arbitrary = ["dep:arbitrary"] default = ["serde"] + +[[bench]] +name = "bench_codec" +harness = false + +[[bench]] +name = "bench_parser" +harness = false diff --git a/benches/bench_codec.rs b/benches/bench_codec.rs new file mode 100644 index 0000000000..253b05d10d --- /dev/null +++ b/benches/bench_codec.rs @@ -0,0 +1,129 @@ +mod helper; + +use bluerobotics_ping::{codec::PingCodec, message::ProtocolMessage}; +use criterion::{ + black_box, criterion_group, criterion_main, AxisScale, BatchSize, BenchmarkId, Criterion, + PlotConfiguration, Throughput, +}; +use futures::{SinkExt, StreamExt}; +use helper::protocol_message_from_messages; +use rand::{rngs::StdRng, SeedableRng}; +use tokio_util::codec::{Decoder, Encoder, FramedRead, FramedWrite}; + +fn benchmark_decode(c: &mut Criterion) { + let seed = 42; + let mut rng: StdRng = SeedableRng::seed_from_u64(seed); + + let mut group = c.benchmark_group("decode"); + group + .measurement_time(std::time::Duration::from_secs(10)) + .significance_level(0.01) + .sample_size(1000) + .plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + for messages_count in &vec![1, 10, 100, 1000, 10000] { + group.throughput(Throughput::Elements(*messages_count)); + + let buffer = helper::create_random_protocol_messages(&mut rng, *messages_count as usize) + .iter() + .map(|protocol_message| protocol_message.serialized()) + .flatten() + .collect::>(); + + group.bench_function(BenchmarkId::new("sync", messages_count), |b| { + b.to_async(&helper::rt()).iter_batched( + || { + let buffer = bytes::BytesMut::from(buffer.as_slice()); + (buffer, PingCodec::new()) + }, + |(mut buffer, mut codec)| async move { + for _ in 0..*messages_count { + let _protocol_message = + black_box(codec.decode(&mut buffer).unwrap().unwrap()); + } + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function(BenchmarkId::new("async-framed", messages_count), |b| { + b.to_async(&helper::rt()).iter_batched( + || { + let codec = PingCodec::new(); + let framed_read = FramedRead::new(buffer.as_slice(), codec); + + framed_read + }, + |mut framed_read| async move { + for _ in 0..*messages_count { + let _protocol_message = + black_box(framed_read.next().await.unwrap().unwrap()); + } + }, + BatchSize::SmallInput, + ) + }); + } + + group.finish(); +} + +fn benchmark_encode(c: &mut Criterion) { + let seed = 42; + let mut rng: StdRng = SeedableRng::seed_from_u64(seed); + + let mut group = c.benchmark_group("encode"); + group + .measurement_time(std::time::Duration::from_secs(10)) + .significance_level(0.1) + .sample_size(1000) + .plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + for messages_count in &vec![1, 10, 100, 1000, 10000] { + group.throughput(Throughput::Elements(*messages_count)); + + let protocol_messages = helper::create_random_messages(&mut rng, *messages_count as usize) + .iter() + .map(protocol_message_from_messages) + .collect::>(); + let buffer: Vec = + Vec::with_capacity(*messages_count as usize * size_of::()); + + group.bench_function(BenchmarkId::new("sync", messages_count), |b| { + b.to_async(&helper::rt()).iter_batched( + || { + let buffer = bytes::BytesMut::from(buffer.as_slice()); + (protocol_messages.clone(), buffer, PingCodec::new()) + }, + |(protocol_messages, mut buffer, mut codec)| async move { + for protocol_message in protocol_messages { + codec.encode(protocol_message, &mut buffer).unwrap(); + } + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function(BenchmarkId::new("async-framed", messages_count), |b| { + b.to_async(&helper::rt()).iter_batched( + || { + let codec = PingCodec::new(); + let framed_write = FramedWrite::new(buffer.clone(), codec); + + (protocol_messages.clone(), framed_write) + }, + |(messages, mut framed_write)| async move { + for protocol_message in messages { + framed_write.send(protocol_message).await.unwrap(); + } + }, + BatchSize::SmallInput, + ) + }); + } + + group.finish(); +} + +criterion_group!(benches, benchmark_decode, benchmark_encode); +criterion_main!(benches); diff --git a/benches/bench_parser.rs b/benches/bench_parser.rs new file mode 100644 index 0000000000..af0599e461 --- /dev/null +++ b/benches/bench_parser.rs @@ -0,0 +1,83 @@ +mod helper; + +use bluerobotics_ping::{message::ProtocolMessage, Messages}; +use criterion::{ + black_box, criterion_group, criterion_main, AxisScale, BatchSize, BenchmarkId, Criterion, + PlotConfiguration, Throughput, +}; +use helper::protocol_message_from_messages; +use rand::{rngs::StdRng, SeedableRng}; + +fn benchmark_protocol_message_to_messages(c: &mut Criterion) { + let seed = 42; + let mut rng: StdRng = SeedableRng::seed_from_u64(seed); + + let mut group = c.benchmark_group("protocol_message_to_messages"); + group + .measurement_time(std::time::Duration::from_secs(10)) + .significance_level(0.01) + .sample_size(1000) + .plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + for messages_count in &vec![1, 100, 10000] { + group.throughput(Throughput::Elements(*messages_count)); + + let protocol_messages = helper::create_random_messages(&mut rng, *messages_count as usize) + .iter() + .map(protocol_message_from_messages) + .collect::>(); + + group.bench_function(BenchmarkId::new("sync", messages_count), |b| { + b.to_async(&helper::rt()).iter_batched( + || protocol_messages.clone(), + |protocol_messages| async move { + for protocol_message in &protocol_messages { + let _message = black_box(Messages::try_from(protocol_message).unwrap()); + } + }, + BatchSize::SmallInput, + ) + }); + } + + group.finish(); +} + +fn benchmark_messages_to_protocol_message(c: &mut Criterion) { + let seed = 42; + let mut rng: StdRng = SeedableRng::seed_from_u64(seed); + + let mut group = c.benchmark_group("messages_to_protocol_message"); + group + .measurement_time(std::time::Duration::from_secs(10)) + .significance_level(0.01) + .sample_size(1000) + .plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + for messages_count in &vec![1, 100, 10000] { + group.throughput(Throughput::Elements(*messages_count)); + + let messages = helper::create_random_messages(&mut rng, *messages_count as usize); + + group.bench_function(BenchmarkId::new("sync", messages_count), |b| { + b.to_async(&helper::rt()).iter_batched( + || messages.clone(), + |messages| async move { + for message in &messages { + let _protocol_message = black_box(protocol_message_from_messages(message)); + } + }, + BatchSize::SmallInput, + ) + }); + } + + group.finish(); +} + +criterion_group!( + benches, + benchmark_protocol_message_to_messages, + benchmark_messages_to_protocol_message +); +criterion_main!(benches); diff --git a/benches/helper.rs b/benches/helper.rs new file mode 100644 index 0000000000..992fbbcbae --- /dev/null +++ b/benches/helper.rs @@ -0,0 +1,55 @@ +use bluerobotics_ping::{message::ProtocolMessage, Messages}; +use rand::rngs::StdRng; +use tokio::runtime::Runtime; + +pub fn rt() -> Runtime { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .thread_name("criterion-tokio-rt") + .build() + .unwrap() +} + +#[cfg(feature = "arbitrary")] +pub fn create_random_messages(mut rng: &mut StdRng, count: usize) -> Vec { + use arbitrary::Arbitrary; + + let mut messages = Vec::with_capacity(count); + + for _ in 0..count { + let mut buf = [0u8; 1024]; // plenty of bytes available + rand::RngCore::fill_bytes(&mut rng, &mut buf); + + let mut u = arbitrary::Unstructured::new(&buf); + + if let Ok(msg) = Messages::arbitrary(&mut u) { + messages.push(msg); + } + } + + messages +} +#[cfg(not(feature = "arbitrary"))] +pub fn create_random_messages(_rng: &mut StdRng, _count: usize) -> Vec { + panic!("Missing 'arbitrary' feature. Re-run it with `--features=arbitrary`") +} + +pub fn create_random_protocol_messages(rng: &mut StdRng, count: usize) -> Vec { + create_random_messages(rng, count) + .iter() + .map(protocol_message_from_messages) + .collect() +} + +#[inline(always)] +pub fn protocol_message_from_messages(message: &Messages) -> ProtocolMessage { + let mut protocol_message = ProtocolMessage::new(); + match message { + Messages::Ping360(message) => protocol_message.set_message(message), + Messages::Omniscan450(message) => protocol_message.set_message(message), + Messages::Bluebps(message) => protocol_message.set_message(message), + Messages::Ping1D(message) => protocol_message.set_message(message), + Messages::Common(message) => protocol_message.set_message(message), + } + protocol_message +} diff --git a/build/binder.rs b/build/binder.rs index 8b937aff9d..919866ebbc 100644 --- a/build/binder.rs +++ b/build/binder.rs @@ -159,6 +159,7 @@ pub fn generate(modules: Vec, out: &mut W) { #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] #enum_ident #try_from_ident diff --git a/build/parser.rs b/build/parser.rs index ea04cf6407..9480312baf 100644 --- a/build/parser.rs +++ b/build/parser.rs @@ -12,12 +12,14 @@ macro_rules! ident { } #[derive(Debug)] +#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] struct VectorType { size_type: Option, data_type: PayloadType, } #[derive(Debug)] +#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] enum PayloadType { CHAR, U8, @@ -92,6 +94,7 @@ impl PayloadType { } #[derive(Debug)] +#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] struct Payload { name: String, description: Option, @@ -141,6 +144,7 @@ impl Payload { } #[derive(Debug)] +#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] struct MessageDefinition { name: String, id: u16, @@ -151,6 +155,7 @@ struct MessageDefinition { } #[derive(Debug, PartialEq)] +#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] enum MessageDefinitionCategory { Set, Get, @@ -451,6 +456,7 @@ impl MessageDefinition { quote! { #[derive(Debug, Clone, PartialEq, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] #[doc = #comment] pub struct #struct_name { #(#variables)* @@ -488,6 +494,7 @@ pub fn emit_protocol_wrapper() -> TokenStream { quote! { #[derive(Debug, Clone, PartialEq, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] pub struct PingProtocolHead { pub source_device_id: u8, pub destiny_device_id: u8, @@ -616,6 +623,7 @@ pub fn generate(input: &mut R, output_rust: &mut W) { let message_enums = quote! { #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))] pub enum Messages { #(#message_enums)* }