feat: add benchmarks, justfile, optimize throughput, harden quality gates

Benchmarking & comparison:
- Add criterion benchmarks (ucobs vs cobs 0.3 vs corncobs 0.1)
- Add Python cobs benchmark script
- Add performance comparison section to README with full results

Performance:
- Rewrite encode to scan-and-bulk-copy via copy_from_slice
- Rewrite decode to bulk-copy via copy_from_slice
- ~2-4x throughput improvement; now matches corncobs on most workloads

Quality hardening:
- Add #![forbid(unsafe_code)], #![warn(missing_docs)], #[must_use]
- Add [lints.rust] unsafe_code = "forbid" in Cargo.toml
- Add rust-version (MSRV) = "1.56"
- Add docs.rs metadata
- 106 tests: 80 unit/corpus + 25 property tests (25k cases each = 625k total)
- External test vectors from: Wikipedia, Cheshire & Baker paper, nanocobs,
  cobs-c, cobs2-rs, Jacques Fortier, Python cobs package
- Add fuzz_small_dest fuzz target (encode/decode with undersized buffers)
- Improve doc examples to show sentinel append/strip convention

Infrastructure:
- Add justfile with test (nextest), check, bench, fuzz, miri, fmt targets
- cargo-nextest for parallel test execution
- Update .gitignore for Python cache files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-30 17:19:12 -06:00
1 parent 08063391e8
commit a5e3e5f63b
10 files changed
+1187 -99

No files matched your search

+149
View File
@@ -0,0 +1,149 @@
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
// ── Test payloads ───────────────────────────────────────────────────
fn payload_zeros(len: usize) -> Vec<u8> {
vec![0u8; len]
}
fn payload_nonzero(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 255 + 1) as u8).collect()
}
fn payload_mixed(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 256) as u8).collect()
}
// ── ucobs ───────────────────────────────────────────────────────────
fn ucobs_encode(data: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; ucobs::max_encoded_len(data.len()) + 1];
let n = ucobs::encode(data, &mut buf).unwrap();
buf.truncate(n);
buf
}
fn ucobs_decode(encoded: &[u8], out: &mut [u8]) -> usize {
ucobs::decode(encoded, out).unwrap()
}
// ── corncobs ────────────────────────────────────────────────────────
fn corncobs_encode(data: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; corncobs::max_encoded_len(data.len())];
let n = corncobs::encode_buf(data, &mut buf);
buf.truncate(n);
buf
}
fn corncobs_decode(encoded: &[u8], out: &mut [u8]) -> usize {
// corncobs expects trailing sentinel
corncobs::decode_buf(encoded, out).unwrap()
}
// ── cobs crate ──────────────────────────────────────────────────────
fn cobs_encode(data: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; cobs::max_encoding_length(data.len())];
let n = cobs::encode(data, &mut buf);
buf.truncate(n);
buf
}
fn cobs_decode(encoded: &[u8], out: &mut [u8]) -> usize {
// unwrap_or(0): the cobs crate returns Err(EmptyFrame) for empty input,
// which is valid COBS. We treat it as 0 decoded bytes for benchmarking.
cobs::decode(encoded, out).unwrap_or(0)
}
// ── Benchmarks ──────────────────────────────────────────────────────
const SIZES: &[usize] = &[0, 1, 10, 64, 254, 255, 256, 1024, 4096];
fn bench_encode(c: &mut Criterion) {
let payloads: Vec<(&str, Box<dyn Fn(usize) -> Vec<u8>>)> = vec![
("zeros", Box::new(payload_zeros)),
("nonzero", Box::new(payload_nonzero)),
("mixed", Box::new(payload_mixed)),
];
for (name, make) in &payloads {
let mut group = c.benchmark_group(format!("encode/{name}"));
for &size in SIZES {
let data = make(size);
group.throughput(Throughput::Bytes(size as u64));
group.bench_with_input(BenchmarkId::new("ucobs", size), &data, |b, d| {
let mut buf = vec![0u8; ucobs::max_encoded_len(d.len()) + 1];
b.iter(|| {
black_box(ucobs::encode(black_box(d), &mut buf).unwrap());
});
});
group.bench_with_input(BenchmarkId::new("corncobs", size), &data, |b, d| {
let mut buf = vec![0u8; corncobs::max_encoded_len(d.len())];
b.iter(|| {
black_box(corncobs::encode_buf(black_box(d), &mut buf));
});
});
group.bench_with_input(BenchmarkId::new("cobs", size), &data, |b, d| {
let mut buf = vec![0u8; cobs::max_encoding_length(d.len())];
b.iter(|| {
black_box(cobs::encode(black_box(d), &mut buf));
});
});
}
group.finish();
}
}
fn bench_decode(c: &mut Criterion) {
let payloads: Vec<(&str, Box<dyn Fn(usize) -> Vec<u8>>)> = vec![
("zeros", Box::new(payload_zeros)),
("nonzero", Box::new(payload_nonzero)),
("mixed", Box::new(payload_mixed)),
];
for (name, make) in &payloads {
let mut group = c.benchmark_group(format!("decode/{name}"));
for &size in SIZES {
let data = make(size);
let ucobs_enc = ucobs_encode(&data);
// corncobs expects sentinel in encoded form
let corncobs_enc = corncobs_encode(&data);
let cobs_enc = cobs_encode(&data);
group.throughput(Throughput::Bytes(size as u64));
group.bench_with_input(BenchmarkId::new("ucobs", size), &ucobs_enc, |b, enc| {
let mut buf = vec![0u8; size + 1];
b.iter(|| {
black_box(ucobs_decode(black_box(enc), &mut buf));
});
});
group.bench_with_input(
BenchmarkId::new("corncobs", size),
&corncobs_enc,
|b, enc| {
let mut buf = vec![0u8; size + 1];
b.iter(|| {
black_box(corncobs_decode(black_box(enc), &mut buf));
});
},
);
group.bench_with_input(BenchmarkId::new("cobs", size), &cobs_enc, |b, enc| {
let mut buf = vec![0u8; size + 1];
b.iter(|| {
black_box(cobs_decode(black_box(enc), &mut buf));
});
});
}
group.finish();
}
}
criterion_group!(benches, bench_encode, bench_decode);
criterion_main!(benches);