Skip to content

perf(waproto): pin the Message codec to one instantiation via non-generic helpers - #842

Merged
jlucaso1 merged 2 commits into
mainfrom
perf/proto-codec-pinning
Jun 11, 2026
Merged

perf(waproto): pin the Message codec to one instantiation via non-generic helpers#842
jlucaso1 merged 2 commits into
mainfrom
perf/proto-codec-pinning

Conversation

@jlucaso1

Copy link
Copy Markdown
Collaborator

Problem

A cargo-bloat audit of the release binary (fat LTO, codegen-units=1) found that 2.5 MiB of the 13.1 MiB .text section is duplicated monomorphization: the same generic function compiled in two or three crates. prost's Message methods are generic, rustc instantiates them in whichever crate calls them, and the per-crate symbols carry distinct instantiating-crate hashes, so even fat LTO cannot merge them. The defining crate (waproto) instantiated about 1 KiB of all that codegen; wacore, the lib and the consuming bin crate paid for the rest.

The worst case was whatsapp::Message::encode_raw::<Vec<u8>>: three full copies at ~160 KiB each. The decode side additionally split by buffer type, with most call sites instantiating the &mut &[u8] tree and a few by-value &[u8] callers dragging in a second one (BotMetadata::merge_field::<&[u8]> alone was 65 KiB of pure duplication).

This affects every consumer the same way: their binary gets its own copies on top of the lib's.

Change

New waproto::codec module with #[inline(never)] non-generic entry points for the hot proto roots:

  • wa::Message: message_encoded_len, message_encode_into, message_to_vec, message_decode
  • wa::WebMessageInfo and wa::HistorySync: pinned decode
  • wa::MessageContextInfo: pinned encode (it is spliced manually on the DM/group send paths)

Every production call site now routes through them (send paths in wacore::messages including the pre-sized splice buffers, receive/edit/comment/msg-secret/sender-keys/newsletter/PDO decode, reporting token, the lazy history-sync accessor). The whole encode and decode tree is codegen'd exactly once, in the defining crate, and decode uses a single buffer shape (&mut &[u8], the one the workspace already instantiated everywhere). #[inline(never)] keeps MIR inlining from re-expanding the bodies at call sites, which would silently reintroduce the per-crate copies. Test-only call sites were left untouched.

Measured

Release bin, same flags as the shipping profile (strip disabled only to read symbols):

  • .text: 13.03 MiB -> 11.85 MiB (-1208 KiB, -9.1%)
  • Message::encode_raw::<Vec<u8>>: 3 copies -> 1
  • cross-crate duplicate-symbol waste (identical demangled symbols, LLVM .NNN clones excluded): 2528 KiB -> 1480 KiB

Runtime cost is one direct call into a function that encodes or decodes the full message tree (micro vs. the body itself); the CodSpeed run on this PR is the neutrality check.

Tests

No behavior change intended; the full wacore + lib suites pass (1838 tests). The codec helpers are exercised by every existing encode/decode test through the converted call sites.

…eric helpers

prost's Message methods are generic, so rustc instantiates them in every crate that calls them; the per-crate symbols carry distinct instantiating-crate hashes that LTO cannot merge, and the release binary shipped three full copies of Message::encode_raw (~160 KiB each, instantiated by wacore, the lib and the consuming bin crate) plus duplicated slices of the decode tree, including a second buffer-type instantiation for callers that decoded from a by-value &[u8].

waproto::codec adds #[inline(never)] non-generic entry points for the hot roots (Message encode/decode, WebMessageInfo and HistorySync decode, MessageContextInfo encode) and every production call site routes through them, so the whole tree is codegen'd once in the defining crate and decode uses a single buffer shape.

Measured on the release bin (fat LTO, cgu=1): .text 13.03 MiB to 11.85 MiB (-1208 KiB, -9.1%); Message::encode_raw from 3 copies to 1; cross-crate duplicate-symbol waste from 2528 KiB to 1480 KiB.
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f2f7c87c-6402-436d-af1b-53a4f8400015

📥 Commits

Reviewing files that changed from the base of the PR and between a4dca2f and 084b77c.

📒 Files selected for processing (1)
  • wacore/src/messages.rs

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Replaced internal message serialization with a unified codec across messaging, newsletter, encryption wrappers, and related utilities. Improves consistency and reliability of message (de)serialization and internal framing without changing public APIs or behavior. No user-facing changes.

Walkthrough

This PR adds a new waproto::codec module exposing non-generic protobuf encode/decode and length helpers, then migrates internal message (de)serialization sites across the codebase to use these codec helpers instead of direct Prost trait calls.

Changes

Protobuf codec abstraction layer migration

Layer / File(s) Summary
Codec module foundation
waproto/src/lib.rs
New codec module exports encode/decode helpers and encoded-length functions for whatsapp::Message, WebMessageInfo, HistorySync, and MessageContextInfo.
Core message utility refactoring
wacore/src/messages.rs
MessageUtils::encode_and_pad, encode_and_pad_with_context, encode_dm_plaintexts, decode_plaintext, and push_message_field switched to waproto::codec for sizing and (de)serialization; spliced tag constants sourced from waproto::tags::*.
Comment and message-edit secret encryption
wacore/src/comment.rs, wacore/src/message_edit.rs
Comment and message-edit encrypt/decrypt paths use codec helpers for inner Message (de)serialization; test modules retain Prost trait imports where required.
Message decoding paths
src/features/newsletter.rs, src/message/msg_secret.rs, src/message/special.rs
Newsletter, msmsg, and special message handlers now call waproto::codec::message_decode for plaintext decoding and remove unused Prost imports.
Sent-message retry cache
src/client/sender_keys.rs
Retry cache L1 consume, DB read, peek, and add paths encode/decode cached message payloads via codec helpers while preserving L1/DB control flow and cleanup semantics.
Newsletter send stanza serialization
src/send.rs
Newsletter edit node and send paths serialize plaintext using waproto::codec::message_to_vec and refactor stanza child construction to an explicit children vector before extending with meta/extra nodes.
PDO, reporting token, and history sync decoding
src/pdo.rs, wacore/src/reporting_token.rs, wacore/src/types/events.rs
PDO placeholder WebMessageInfo, reporting-token content generation, and LazyHistorySync decoding now use codec decoders/serializers; module imports adjusted for tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

(Note: I expect this to land cleanly if encoding/decoding behavior is identical; verify decode error paths and test coverage.)

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title precisely describes the main change: pinning Message codec to non-generic helpers to reduce codegen duplication.
Description check ✅ Passed The description thoroughly explains the problem (2.5 MiB codegen duplication), solution (waproto::codec module with non-generic helpers), measured impact (-1208 KiB, -9.1%), and testing status (1838 tests pass).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/proto-codec-pinning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq

codspeed-hq Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 141 untouched benchmarks


Comparing perf/proto-codec-pinning (084b77c) with main (4edba60)

Open in CodSpeed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/src/messages.rs`:
- Around line 50-53: Replace the hardcoded protobuf field tag constants (e.g.
TAG_MESSAGE_CONTEXT_INFO) used in manual wire framing calls (such as
len_delimited_len(...) calling waproto::codec::message_context_info_encoded_len)
with the generated tag constants from the schema (use the constants in the
waproto::tags module, e.g. waproto::tags::TAG_MESSAGE_CONTEXT_INFO); apply the
same substitution for the other occurrences called out (the usages at the other
ranges) so the manual framing uses the canonical generated tags, or
alternatively add compile-time asserts that the local TAG_* values equal
waproto::tags::* to prevent silent renumbering. Ensure you update all uses
(recipient/DSM helpers) referencing TAG_* in this file to reference
waproto::tags::* or add the asserts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d19df05c-d430-4964-8065-fe1b9da7b28b

📥 Commits

Reviewing files that changed from the base of the PR and between 4edba60 and a4dca2f.

📒 Files selected for processing (12)
  • src/client/sender_keys.rs
  • src/features/newsletter.rs
  • src/message/msg_secret.rs
  • src/message/special.rs
  • src/pdo.rs
  • src/send.rs
  • wacore/src/comment.rs
  • wacore/src/message_edit.rs
  • wacore/src/messages.rs
  • wacore/src/reporting_token.rs
  • wacore/src/types/events.rs
  • waproto/src/lib.rs

Comment thread wacore/src/messages.rs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 12 files

Re-trigger cubic

…a constants

The hand-written recipient/DSM framing hardcoded the four field numbers it splices. Deriving them from waproto::tags turns a .proto renumber into a compile error here instead of a silently changed wire payload; the splice_* differential tests keep pinning the framing itself against prost.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant