#![allow(clippy::unwrap_used)] //! Benchmark: Cap'n Proto vs Protobuf serialization for chat message envelopes. //! //! Compares serialization/deserialization speed and encoded size at three //! payload sizes (100 B, 1 KB, 4 KB) for a typical Envelope{seq, data} message. use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; // ── Cap'n Proto path ──────────────────────────────────────────────────────── fn capnp_serialize_envelope(seq: u64, data: &[u8]) -> Vec { let mut msg = capnp::message::Builder::new_default(); { let mut envelope = msg.init_root::(); envelope.set_seq(seq); envelope.set_data(data); } quicproquo_proto::to_bytes(&msg).unwrap() } fn capnp_deserialize_envelope(bytes: &[u8]) -> (u64, Vec) { let reader = quicproquo_proto::from_bytes(bytes).unwrap(); let envelope = reader .get_root::() .unwrap(); (envelope.get_seq(), envelope.get_data().unwrap().to_vec()) } // ── Protobuf path (hand-coded prost encoding to avoid build-dep) ──────────── // // Envelope { seq: uint64 (field 1), data: bytes (field 2) } // Wire format: varint tag + varint seq + len-delimited data fn protobuf_serialize_envelope(seq: u64, data: &[u8]) -> Vec { // Build a prost message via raw encoding. // Field 1: uint64 seq, wire type 0 (varint), tag = (1 << 3) | 0 = 0x08 // Field 2: bytes data, wire type 2 (length-delimited), tag = (2 << 3) | 2 = 0x12 let mut buf = Vec::with_capacity(10 + data.len()); // Encode field 1 (seq) prost::encoding::uint64::encode(1, &seq, &mut buf); // Encode field 2 (data) prost::encoding::bytes::encode(2, &data.to_vec(), &mut buf); buf } fn protobuf_deserialize_envelope(bytes: &[u8]) -> (u64, Vec) { // Decode manually using prost wire format let mut seq: u64 = 0; let mut data: Vec = Vec::new(); let mut buf = bytes; while !buf.is_empty() { let (tag, wire_type) = prost::encoding::decode_key(&mut buf).expect("decode key"); match tag { 1 => { prost::encoding::uint64::merge(wire_type, &mut seq, &mut buf, Default::default()) .expect("decode seq"); } 2 => { prost::encoding::bytes::merge(wire_type, &mut data, &mut buf, Default::default()) .expect("decode data"); } _ => { prost::encoding::skip_field(wire_type, tag, &mut buf, Default::default()) .expect("skip unknown field"); } } } (seq, data) } // ── Benchmarks ────────────────────────────────────────────────────────────── fn bench_serialize(c: &mut Criterion) { let sizes: &[(&str, usize)] = &[("100B", 100), ("1KB", 1024), ("4KB", 4096)]; let mut group = c.benchmark_group("serialize_envelope"); for (label, size) in sizes { let payload = vec![0xABu8; *size]; let seq = 42u64; group.bench_with_input( BenchmarkId::new("capnp", label), &(&seq, &payload), |b, &(seq, payload)| { b.iter(|| capnp_serialize_envelope(black_box(*seq), black_box(payload))); }, ); group.bench_with_input( BenchmarkId::new("protobuf", label), &(&seq, &payload), |b, &(seq, payload)| { b.iter(|| protobuf_serialize_envelope(black_box(*seq), black_box(payload))); }, ); } group.finish(); } fn bench_deserialize(c: &mut Criterion) { let sizes: &[(&str, usize)] = &[("100B", 100), ("1KB", 1024), ("4KB", 4096)]; let mut group = c.benchmark_group("deserialize_envelope"); for (label, size) in sizes { let payload = vec![0xABu8; *size]; let seq = 42u64; let capnp_bytes = capnp_serialize_envelope(seq, &payload); let proto_bytes = protobuf_serialize_envelope(seq, &payload); group.bench_with_input( BenchmarkId::new("capnp", label), &capnp_bytes, |b, bytes| { b.iter(|| capnp_deserialize_envelope(black_box(bytes))); }, ); group.bench_with_input( BenchmarkId::new("protobuf", label), &proto_bytes, |b, bytes| { b.iter(|| protobuf_deserialize_envelope(black_box(bytes))); }, ); } group.finish(); } fn bench_encoded_sizes(c: &mut Criterion) { let sizes: &[(&str, usize)] = &[("100B", 100), ("1KB", 1024), ("4KB", 4096)]; let mut group = c.benchmark_group("encoded_size"); for (label, size) in sizes { let payload = vec![0xABu8; *size]; let capnp_bytes = capnp_serialize_envelope(42, &payload); let proto_bytes = protobuf_serialize_envelope(42, &payload); // Use a trivial benchmark that just returns the size -- the point // is to get criterion to print the iteration count and allow // comparison. The real value is in the eprintln below. group.bench_with_input( BenchmarkId::new("capnp", label), &capnp_bytes, |b, bytes| { b.iter(|| black_box(bytes.len())); }, ); group.bench_with_input( BenchmarkId::new("protobuf", label), &proto_bytes, |b, bytes| { b.iter(|| black_box(bytes.len())); }, ); eprintln!( " {label}: capnp={} bytes, protobuf={} bytes, overhead={:+} bytes", capnp_bytes.len(), proto_bytes.len(), capnp_bytes.len() as isize - proto_bytes.len() as isize, ); } group.finish(); } criterion_group!(benches, bench_serialize, bench_deserialize, bench_encoded_sizes); criterion_main!(benches);