172 lines
6.1 KiB
Rust
172 lines
6.1 KiB
Rust
#![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<u8> {
|
|
let mut msg = capnp::message::Builder::new_default();
|
|
{
|
|
let mut envelope = msg.init_root::<quicproquo_proto::node_capnp::envelope::Builder>();
|
|
envelope.set_seq(seq);
|
|
envelope.set_data(data);
|
|
}
|
|
quicproquo_proto::to_bytes(&msg).unwrap()
|
|
}
|
|
|
|
fn capnp_deserialize_envelope(bytes: &[u8]) -> (u64, Vec<u8>) {
|
|
let reader = quicproquo_proto::from_bytes(bytes).unwrap();
|
|
let envelope = reader
|
|
.get_root::<quicproquo_proto::node_capnp::envelope::Reader>()
|
|
.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<u8> {
|
|
// 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<u8>) {
|
|
// Decode manually using prost wire format
|
|
let mut seq: u64 = 0;
|
|
let mut data: Vec<u8> = 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);
|