anthropics / anthropics/buffa

buffa in production: full prost->buffa migration (+21.65%) — memory & binary-size tradeoffs worth investigating

Offen
#270 1 Kommentar 1 Reaktion 0 zugewiesene Personen Auf GitHub ansehen
Vorherrschende Sprache
Rust
Sterne
883
Forks
88
Ø Merge
3 T. 19 Std.
Gemergte PRs (30 T.)
42

Beschreibung

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`, `EnumValue`, 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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.