Skip to content

perf: keep droppy error/default construction off per-message happy paths - #952

Merged
jlucaso1 merged 1 commit into
mainfrom
claude/whatsapp-rust-allocator-api-prw9c4
Jul 2, 2026
Merged

jlucaso1 merged 1 commit into
mainfrom
claude/whatsapp-rust-allocator-api-prw9c4

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #949, applying the same eager-construction fix to the three remaining hot-path families found by mining the CodSpeed flamegraphs of the heaviest benchmarks plus a code sweep:

1. libsignal message parsing (runs per inbound message). SignalProtocolError carries drop glue (InvalidArgument(String), BackendError(_, Box<dyn Error>)…), so each .ok_or(SignalProtocolError::...) on a present-field path constructed the enum and paid its drop:

  • SignalMessage::try_from / decode_ciphertext — every 1:1 message
  • PreKeySignalMessage::try_from — every pkmsg
  • SenderKeyMessage::try_from / decode_ciphertext — every group message
  • group_cipher.rs sender_chain_key() lookups — group encrypt/decrypt/SKDM build. The format! siblings on adjacent lines already use ok_or_else, confirming these unit-variant sites were simply missed.
  • sender_keys.rs seed_to_array — per cached message key on record load (every group decrypt)

2. Decoder attribute loop (hottest loop in the receive path). read_attributes built the default ValueRef::String(NodeStr::Borrowed("")) eagerly per attribute via unwrap_orValueRef has drop glue via CompactString. This site was missed by #949.

3. collect_unique_index_macs (appstate patch processing). The large-patch path collected owned Vec<u8> MACs through a realloc-grown Vec (flamegraph: ~12% in the realloc chain), then sorted the 24-byte headers. Now sorts/dedups borrowed slices and materializes only the keepers into an exact-sized Vec — no realloc growth, no alloc+drop for duplicates, 16-byte scratch elements. Public signature unchanged.

As in #949, let-else/match is used instead of ok_or_else/unwrap_or_else because clippy's unnecessary_lazy_evaluations reverts the lazy form for unit variants, ignoring drop-glue cost.

No behavior change — same error variants on the same failure conditions, same MAC set returned.

Validation

  • cargo fmt / cargo clippy --all-targets on affected crates: clean (only the pre-existing chrono::Local::now warning, untouched by this diff)
  • cargo test -p wacore-libsignal -p wacore-binary -p wacore: all pass (239+ tests)
  • Local wall-time A/B is within noise, as expected for drop-glue-scale effects on crypto-dominated benches — CodSpeed's deterministic instruction counting on this PR is the arbiter (same as perf(binary): keep BinaryError construction off the decoder happy path #949, which measured -7.2% on bench_unmarshal_large)
  • Benches to watch: bench_dm_decrypt_subsequent_message, bench_group_decrypt/encrypt_message, bench_attr_parser, bench_unmarshal_*, bench_collect_unique_index_macs[1000]

🤖 Generated with Claude Code

https://claude.ai/code/session_01T53MPuGRg4kvRjRxW6fUvd


Generated by Claude Code

Same family as #949, now on the paths that run per inbound message.
SignalProtocolError carries drop glue (String and Box<dyn Error>
variants), so every ok_or(SignalProtocolError::...) on a present-field
path built the enum and paid its drop:

- SignalMessage/PreKeySignalMessage/SenderKeyMessage try_from and
  decode_ciphertext (every 1:1 and group message parse)
- group_cipher sender_chain_key lookups (group encrypt/decrypt/skdm);
  the format! siblings there already used ok_or_else
- sender_keys seed_to_array (per cached key on record load)

Also read_attributes built a default ValueRef eagerly per attribute via
unwrap_or, and collect_unique_index_macs sorted owned Vec<u8> MACs after
a realloc-grown collect: sort borrowed slices first, then materialize
only the keepers into an exact-sized Vec.

let-else/match instead of ok_or_else because clippy's
unnecessary_lazy_evaluations would revert the lazy form for unit
variants, ignoring drop-glue cost (see #949).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T53MPuGRg4kvRjRxW6fUvd
@coderabbitai

coderabbitai Bot commented Jul 2, 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 (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fb3ec025-d72a-496e-9f38-f59b26f00fe7

📥 Commits

Reviewing files that changed from the base of the PR and between 0ce4907 and cbea780.

📒 Files selected for processing (5)
  • wacore/binary/src/decoder.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/src/appstate_sync.rs

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of missing or malformed protocol fields during message decoding, reducing parsing failures and making error handling more consistent.
    • Preserved the same early error behavior when required sender key data is unavailable.
    • Reduced allocation overhead when deduplicating app state sync data, which may improve performance for larger updates.

Walkthrough

This PR refactors error-handling patterns from .ok_or(...)/.unwrap_or(...) combinator chains to explicit let Some(...) else and match constructs across decoder, group cipher, protocol, and sender key modules, preserving identical error types and control flow. Separately, appstate_sync.rs's large-patch MAC deduplication is rewritten to sort/dedup borrowed slices before allocating owned vectors.

Changes

Option/Result Pattern Refactor and Dedup Allocation

Layer / File(s) Summary
Decoder attribute defaulting
wacore/binary/src/decoder.rs
read_attributes uses explicit match instead of unwrap_or to substitute empty-string ValueRef when a value is absent.
Group cipher sender chain key extraction
wacore/libsignal/src/protocol/group_cipher.rs
group_encrypt, get_sender_key, and build_skdm_from_record replace .ok_or(...) with let Some(...) else returning InvalidSenderKeySession.
Protocol message field/ciphertext parsing
wacore/libsignal/src/protocol/protocol.rs
decode_ciphertext and TryFrom<&[u8]> for SignalMessage, PreKeySignalMessage, SenderKeyMessage replace .ok_or(...)/.map(...) chains with match/let Some(...) else, returning InvalidProtobufEncoding on missing fields.
Sender key seed extraction
wacore/libsignal/src/protocol/sender_keys.rs
seed_to_array replaces seed.ok_or(...)? with explicit let Some(seed) = seed else returning InvalidProtobufEncoding.
Appstate MAC dedup allocation rewrite
wacore/src/appstate_sync.rs
collect_unique_index_macs large-patch path now sorts/dedups borrowed &[u8] slices before allocating the final owned Vec<Vec<u8>>, with updated documentation.

Estimated code review effort: 2 (Simple) | ~12 minutes

Possibly related PRs

Suggested labels: performance

Look, this diff is basically stripping out combinator soup and replacing it with let Some(...) else, which is more explicit and, frankly, that's what good engineering looks like — clean, direct, no wasted cycles. The dedup rewrite in appstate_sync also cuts unnecessary allocations, which I respect deeply because efficiency is not optional at scale. This is a small, focused, well-executed refactor. Ship it.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: reducing eager error/default construction on hot paths for performance.
Description check ✅ Passed The description matches the refactor and explains the same hot-path performance fixes and validation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/whatsapp-rust-allocator-api-prw9c4

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.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This is a performance follow-up to #949 that eliminates eager construction of drop-glue-carrying error types and default values on hot message-processing paths, and replaces an owned-MAC sort/dedup with a borrow-sort-then-materialize approach in collect_unique_index_macs.

  • libsignal ok_orlet-else: Converts 9 sites across protocol.rs, group_cipher.rs, and sender_keys.rs where ok_or(SignalProtocolError::…) eagerly constructed an enum variant whose type carries drop glue (due to heap-owning sibling variants), paying drop cost even on the happy path.
  • decoder.rs attribute loop: Replaces unwrap_or(ValueRef::String(…)) with a match so the CompactString-backed default is constructed only when the field is absent, not on every attribute parse.
  • collect_unique_index_macs large-patch path: Sorts 16-byte &[u8] fat pointers (no allocation), deduplicates before ownership, then materializes only unique MACs into an exactly-sized Vec — eliminating per-duplicate to_vec allocations and the post-dedup over-capacity of the old approach.

Confidence Score: 5/5

All five changed files are drop-in mechanical refactors or a well-bounded algorithmic improvement, with no behavior changes and full test coverage in place.

Every ok_or to let-else conversion is semantically identical and verified by the existing 239+ test suite. The collect_unique_index_macs rewrite correctly sorts borrowed slices and materializes only unique MACs; lifetimes are compiler-enforced, and dedup_tests cover the boundary between the linear-scan and sort paths at n=64 and n=65. No public APIs change, no unsafe code is introduced, and clippy is reported clean.

No files require special attention; all changes are straightforward and well-covered by tests.

Important Files Changed

Filename Overview
wacore/binary/src/decoder.rs One-liner change: unwrap_or(ValueRef::String) to match, avoiding eagerly constructing the CompactString-backed default on every attribute in the hot attribute-parsing loop. Semantically identical.
wacore/libsignal/src/protocol/group_cipher.rs Three ok_or(SignalProtocolError::InvalidSenderKeySession) to let-else conversions on the encrypt, decrypt, and SKDM-build paths. Semantically identical; avoids drop-glue construction on the happy path.
wacore/libsignal/src/protocol/protocol.rs Six ok_or(SignalProtocolError::InvalidProtobufEncoding) to let-else conversions across SignalMessage, PreKeySignalMessage, and SenderKeyMessage parsing. All changes are mechanically equivalent.
wacore/libsignal/src/protocol/sender_keys.rs Single ok_or to let-else in seed_to_array, which runs on every cached message key during group decrypt record load. Semantically identical.
wacore/src/appstate_sync.rs Large-patch path of collect_unique_index_macs now sorts/deduplicates borrowed &[u8] fat pointers before materializing unique MACs into an exactly-sized Vec, eliminating per-duplicate to_vec allocations. Public signature and test coverage unchanged.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Inbound message] --> B{Message type?}
    B -->|1:1| C[SignalMessage::try_from]
    B -->|PreKey| D[PreKeySignalMessage::try_from]
    B -->|Group| E[SenderKeyMessage::try_from]
    C --> C1[let Some ratchet_key else Err]
    C1 --> C2[let Some counter else Err]
    C2 --> C3[let Some ciphertext else Err]
    D --> D1[let Some base_key else Err]
    D1 --> D2[let Some identity_key else Err]
    D2 --> D3[let Some message else Err]
    E --> E1[let Some chain_id else Err]
    E1 --> E2[let Some iteration else Err]
    E2 --> E3[let Some ciphertext else Err]
    F[Group encrypt/decrypt] --> G[group_cipher.rs]
    G --> G1[let Some sender_chain_key else Err]
    G1 --> G2[step_with_message_key]
    H[collect_unique_index_macs large patch] --> H1[Vec of borrowed fat pointers]
    H1 --> H2[sort_unstable by byte content]
    H2 --> H3[dedup - no alloc dropped]
    H3 --> H4[collect into exact-sized Vec]
    I[read_attributes hot loop] --> I1[match read_value]
    I1 -->|Some| I2[use value directly]
    I1 -->|None| I3[construct default ValueRef]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Inbound message] --> B{Message type?}
    B -->|1:1| C[SignalMessage::try_from]
    B -->|PreKey| D[PreKeySignalMessage::try_from]
    B -->|Group| E[SenderKeyMessage::try_from]
    C --> C1[let Some ratchet_key else Err]
    C1 --> C2[let Some counter else Err]
    C2 --> C3[let Some ciphertext else Err]
    D --> D1[let Some base_key else Err]
    D1 --> D2[let Some identity_key else Err]
    D2 --> D3[let Some message else Err]
    E --> E1[let Some chain_id else Err]
    E1 --> E2[let Some iteration else Err]
    E2 --> E3[let Some ciphertext else Err]
    F[Group encrypt/decrypt] --> G[group_cipher.rs]
    G --> G1[let Some sender_chain_key else Err]
    G1 --> G2[step_with_message_key]
    H[collect_unique_index_macs large patch] --> H1[Vec of borrowed fat pointers]
    H1 --> H2[sort_unstable by byte content]
    H2 --> H3[dedup - no alloc dropped]
    H3 --> H4[collect into exact-sized Vec]
    I[read_attributes hot loop] --> I1[match read_value]
    I1 -->|Some| I2[use value directly]
    I1 -->|None| I3[construct default ValueRef]
Loading

Reviews (1): Last reviewed commit: "perf: keep droppy error/default construc..." | Re-trigger Greptile

@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 5 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Auto-approved: Performance optimization replacing eager error/default construction with lazy alternatives in hot paths. No behavior change.

Re-trigger cubic

@codspeed-hq

codspeed-hq Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 3 (👁 3) regressed benchmarks
✅ 184 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation send_and_receive[1] 1,169.1 µs 823.9 µs +41.9%
👁 Memory bench_collect_unique_index_macs[1000] 55.3 KB 70.3 KB -21.42%
👁 Simulation bench_unpack_uncompressed 183.1 ns 241.4 ns -24.17%
👁 Simulation reconnect 2.1 ms 2.3 ms -10.45%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/whatsapp-rust-allocator-api-prw9c4 (cbea780) with main (0ce4907)

Open in CodSpeed

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.62 MiB 10.62 MiB -704 B (-0.01%) 🔽
bin .text 8.65 MiB 8.65 MiB -704 B (-0.01%) 🔽
bin allocated (text+data+bss) 10.62 MiB 10.62 MiB -40 B (-0.00%) 🔽
llvm-lines wacore 498,015 498,667 +652 (+0.13%) 🔺
llvm-lines wacore copies 17,045 17,076 +31 (+0.18%) 🔺
llvm-lines whatsapp-rust lib 714,243 714,243 0
llvm-lines whatsapp-rust lib copies 23,185 23,185 0
deps crates (Cargo.lock) 467 467 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.54 MiB 1.54 MiB -315 B (-0.02%) 🔽
.text wacore 532.83 KiB 532.54 KiB -300 B (-0.05%) 🔽
.text wacore_binary 157.58 KiB 157.67 KiB +95 B (+0.06%) 🔺
.text wacore_libsignal 176.46 KiB 176.32 KiB -139 B (-0.08%) 🔽
.text wacore_appstate 156.10 KiB 156.10 KiB 0
.text wacore_noise 26.05 KiB 26.05 KiB 0
.text waproto 1.59 MiB 1.59 MiB 0
.text whatsapp_rust_sqlite_storage 479.73 KiB 479.73 KiB 0
.text whatsapp_rust_tokio_transport 43.46 KiB 43.46 KiB 0
.text whatsapp_rust_ureq_http_client 9.05 KiB 9.05 KiB 0
.text std 1007.48 KiB 1007.45 KiB -34 B (-0.00%) 🔽
.text other deps 2.94 MiB 2.94 MiB 0

Baseline: 0ce4907b2 (latest main run) · Head: ad930cd9a · Graphs

@jlucaso1
jlucaso1 merged commit d4e3d55 into main Jul 2, 2026
18 checks passed
@jlucaso1
jlucaso1 deleted the claude/whatsapp-rust-allocator-api-prw9c4 branch July 2, 2026 22:42
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.

2 participants