Hey — the full prost 0.13 -> buffa 0.8.1 migration I've been chipping away at is now landing in production (a large WhatsApp protocol implementation), so I wanted to share the end-to-end results as a real-world "buffa in production" data point, and flag two tradeoffs that look like they might be addressable in buffa itself rather than at the app level.
Overall it's a clear win — the full CodSpeed suite came out +21.65% net across 179 benchmarks (17 improvements, 6 regressions, 156 unchanged), on top of the struct-size and decode-speed wins from the earlier issues. Sharing the regressions too so the tradeoff is on the table and this isn't one-sided.
PR (real production migration, reproducible CodSpeed run attached to it): oxidezap/whatsapp-rust#557
The wins (so it's not one-sided)
A sample of the big movers vs prost on the same suite:
| benchmark |
metric |
prost |
buffa |
Δ |
decode_plaintext[text_reply] |
memory |
3.4 KB |
1.3 KB |
×2.6 |
decode_plaintext[group_skdm_text] |
time |
8.4 µs |
6.0 µs |
+40% |
history_sync_stream_drain |
memory |
431 KB |
243 KB |
+77% |
process_patch_50_validated |
memory |
171 KB |
48 KB |
×3.6 |
dm_session_establishment |
memory |
3.7 KB |
2.4 KB |
+55% |
full_dm_conversation |
memory |
9.8 KB |
7.8 KB |
+27% |
decode_record[contact] |
time |
44.6 µs |
35.2 µs |
+27% |
Decode across the board is both faster and lighter, and the deep-tree history-sync/appstate paths improved the most — exactly what the boxed-field + tighter-codegen work was aiming at.
Tradeoff 1: a subset of decode/encode shapes regressed on memory
A handful of benchmarks allocate more under buffa than prost. Two shapes stand out:
| benchmark |
metric |
prost |
buffa |
Δ |
decode_record[contact] |
memory |
385 B |
1,113 B |
−65% |
decode_record[star] |
memory |
484 B |
1,085 B |
−55% |
group_send_skdm_256 |
memory |
415.5 KB |
461.8 KB |
−10% |
group_send_skdm_50 |
memory |
73.4 KB |
82.5 KB |
−11% |
The decode_record case is the interesting one: memory ~3x but time −25% (it's in the wins table above), so it's a clean time/memory trade rather than a straight loss — buffa is doing fewer passes but allocating more per decode. The group-send fanout cases allocate ~10% more per encoded recipient.
I haven't pinned the exact source, but the candidates all live in buffa: eager materialization of small nested/repeated fields during decode, per-field MessageField box allocations, or SizeCache spill growth. Worth a look at whether the extra allocation on these small-message decode paths is inherent to buffa's representation or is reclaimable — the time is already better, so closing the memory gap would make these strict wins.
Tradeoff 2: final binary grew ~+3.85% despite codegen shrinking 23%
This is the one I'd most like a second opinion on, because the two halves point in opposite directions:
Metric prost buffa Δ
bin size (stripped) 10.20 MiB 10.59 MiB +401.91 KiB (+3.85%)
bin .text 8.24 MiB 8.63 MiB +397.62 KiB (+4.71%)
llvm-lines wacore (proto crate) 649,184 497,172 -152,012 (-23.42%)
llvm-lines wacore copies 17,998 17,033 -965 (-5.36%)
llvm-lines whatsapp-rust lib 678,181 710,609 +32,428 (+4.78%)
llvm-lines whatsapp-rust lib copies 21,000 23,096 +2,096 (+9.98%)
The generated proto crate got 23% leaner in llvm-lines (buffa's codegen is tighter than prost's) — but the final binary still grew ~400 KiB, and the growth tracks the consuming crate: whatsapp-rust lib llvm-lines +4.78% and, tellingly, monomorphization copies +9.98%. So the extra .text isn't in the generated code — it's in more monomorphized instances downstream.
My working hypothesis is that buffa's encode/decode entry points are generic over the buffer (impl Buf / impl BufMut) and the message/field wrappers are generic (MessageField<T>, EnumValue<E>, the view types), so each concrete message type instantiates those generics in the consuming crate — whereas prost's hot paths are more concrete (bytes::Bytes in fixed positions). With this repo's ~hundreds of message types, that multiplies. But that's a guess — the copies +9.98% number is the concrete signal and I'd rather hear from someone who knows the codegen where those instances are coming from.
Open questions / directions (leaving the fix open):
- Could the per-message
encode/decode public entry points take a concrete buffer type (or &mut dyn Buf) at the boundary and only go generic internally, so N message types don't each monomorphize the buffer generic?
- Is the +4.78% consumer-side growth mostly the
MessageField/EnumValue/view generic instantiations, or the per-message compute_size/write_to/merge_field bodies?
- Is any of this a codegen
#[inline] policy that could be relaxed to dedup across message types?
Repro is the PR itself (the Binary-Size and CodSpeed jobs run on every push, numbers above are from that CI); happy to pull any specific cargo llvm-lines / cargo bloat breakdown from the tree if it helps localize where the copies land.
Hey — the full prost 0.13 -> buffa 0.8.1 migration I've been chipping away at is now landing in production (a large WhatsApp protocol implementation), so I wanted to share the end-to-end results as a real-world "buffa in production" data point, and flag two tradeoffs that look like they might be addressable in buffa itself rather than at the app level.
Overall it's a clear win — the full CodSpeed suite came out +21.65% net across 179 benchmarks (17 improvements, 6 regressions, 156 unchanged), on top of the struct-size and decode-speed wins from the earlier issues. Sharing the regressions too so the tradeoff is on the table and this isn't one-sided.
PR (real production migration, reproducible CodSpeed run attached to it): oxidezap/whatsapp-rust#557
The wins (so it's not one-sided)
A sample of the big movers vs prost on the same suite:
decode_plaintext[text_reply]decode_plaintext[group_skdm_text]history_sync_stream_drainprocess_patch_50_validateddm_session_establishmentfull_dm_conversationdecode_record[contact]Decode across the board is both faster and lighter, and the deep-tree history-sync/appstate paths improved the most — exactly what the boxed-field + tighter-codegen work was aiming at.
Tradeoff 1: a subset of decode/encode shapes regressed on memory
A handful of benchmarks allocate more under buffa than prost. Two shapes stand out:
decode_record[contact]decode_record[star]group_send_skdm_256group_send_skdm_50The
decode_recordcase is the interesting one: memory ~3x but time −25% (it's in the wins table above), so it's a clean time/memory trade rather than a straight loss — buffa is doing fewer passes but allocating more per decode. The group-send fanout cases allocate ~10% more per encoded recipient.I haven't pinned the exact source, but the candidates all live in buffa: eager materialization of small nested/repeated fields during decode, per-field
MessageFieldbox allocations, orSizeCachespill growth. Worth a look at whether the extra allocation on these small-message decode paths is inherent to buffa's representation or is reclaimable — the time is already better, so closing the memory gap would make these strict wins.Tradeoff 2: final binary grew ~+3.85% despite codegen shrinking 23%
This is the one I'd most like a second opinion on, because the two halves point in opposite directions:
The generated proto crate got 23% leaner in llvm-lines (buffa's codegen is tighter than prost's) — but the final binary still grew ~400 KiB, and the growth tracks the consuming crate:
whatsapp-rustlib llvm-lines +4.78% and, tellingly, monomorphization copies +9.98%. So the extra.textisn't in the generated code — it's in more monomorphized instances downstream.My working hypothesis is that buffa's encode/decode entry points are generic over the buffer (
impl Buf/impl BufMut) and the message/field wrappers are generic (MessageField<T>,EnumValue<E>, the view types), so each concrete message type instantiates those generics in the consuming crate — whereas prost's hot paths are more concrete (bytes::Bytesin fixed positions). With this repo's ~hundreds of message types, that multiplies. But that's a guess — thecopies +9.98%number is the concrete signal and I'd rather hear from someone who knows the codegen where those instances are coming from.Open questions / directions (leaving the fix open):
encode/decodepublic entry points take a concrete buffer type (or&mut dyn Buf) at the boundary and only go generic internally, so N message types don't each monomorphize the buffer generic?MessageField/EnumValue/view generic instantiations, or the per-messagecompute_size/write_to/merge_fieldbodies?#[inline]policy that could be relaxed to dedup across message types?Repro is the PR itself (the Binary-Size and CodSpeed jobs run on every push, numbers above are from that CI); happy to pull any specific
cargo llvm-lines/cargo bloatbreakdown from the tree if it helps localize where the copies land.