Skip to content

perf!: cut clones/allocs in LID resolution, single-device send, history-sync - #690

Merged
jlucaso1 merged 3 commits into
mainfrom
perf/lid-send-history-cleanups
Jun 2, 2026
Merged

perf!: cut clones/allocs in LID resolution, single-device send, history-sync#690
jlucaso1 merged 3 commits into
mainfrom
perf/lid-send-history-cleanups

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Three independent allocation/clone cleanups (breaking signature changes are fine pre-1.0).

M1 — LID lookups return CompactString, not a cloned String

The LID/PN cache stores Arc<LidPnEntry>, but get_current_lid deep-cloned the entry's String on every call. It (and SendContextResolver::get_lid_for_phone + IsOnWhatsAppUser.known_lid) now return CompactString, which is inline for the typical ~15-digit LID user — no heap allocation. The JID user field is already CompactString, so the common "build a LID JID from the lookup" callers no longer convert at all. This runs per-recipient during group-send fanout and per-destination on DMs.

C3 — single-recipient send skips the parallel fan-out

encrypt_for_devices always built an Arc<[u8]> copy of the plaintext, spawned a task + oneshot channel per device, and ran a FuturesUnordered — even for one device, where there's no parallelism to gain. A single recipient now encrypts inline. The per-device encrypt and the result-handling are factored into shared helpers (encrypt_one_device, push_encrypt_result) used by both the fast path and the parallel path, so behavior is identical.

C5 — drop the history-sync pre-count pass

process_history_sync (retain-blob path) scanned the whole decompressed blob once just to size the secret-record Vec. But it counted messages while the Vec only holds the secret-record subset, so it over-allocated and paid a full extra scan. Plain growth is cheaper.

Measured (iai): removing the count pass = −2.5% instructions on a 20k-message decode. M1/C3 are structural alloc/overhead removals (a heap String clone per LID lookup; an Arc copy + task spawn + 2 store clones per single-device send).

Breaking

get_current_lid, get_lid_for_phone, and IsOnWhatsAppUser.known_lid change StringCompactString.

Tests

cargo test -p wacore -p whatsapp-rust (1500+ tests incl. send fan-out, lid_pn_cache, usync, history_sync) pass; clippy --all-targets clean.

…ry-sync

M1: get_current_lid / SendContextResolver::get_lid_for_phone / IsOnWhatsAppUser.known_lid now return CompactString (inline for typical ~15-digit LIDs) instead of deep-cloning a String from the Arc-shared cache entry; the JID user field is CompactString, so callers building JIDs no longer convert. M3-adjacent C3: a single-recipient send now encrypts inline, skipping the parallel fan-out's Arc<[u8]> plaintext copy, spawned task + oneshot channel, FuturesUnordered, and two store clones (no parallelism is lost with one device); the per-device encrypt + result-handling are factored into shared helpers so both paths behave identically. C5: drop the history-sync pre-count pass (measured -2.5% of decode) - it scanned the whole blob to size a Vec that only holds the secret-record subset, over-allocating; plain growth is cheaper. Breaking: CompactString return types pre-1.0.
@coderabbitai

coderabbitai Bot commented Jun 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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ddc06682-9382-4872-b142-6624346cf11c

📥 Commits

Reviewing files that changed from the base of the PR and between 23d3b8b and 62d78a9.

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

📝 Walkthrough

Summary by CodeRabbit

  • Performance Improvements

    • Faster encryption for single-recipient messages via a new direct fast path.
    • Reduced memory allocations across lookup, token and message handling paths.
  • Refactor

    • Simplified internal data handling and conversion for cleaner flows.
    • Removed an unnecessary pre-sizing step in message processing to streamline logic.

Walkthrough

This PR converts LID-related fields and returns from String to wacore_binary::CompactString across trait, cache, client, and usync; refactors wacore send per-device encryption into helpers with a single-recipient fast path; and removes history-sync pre-sizing helpers.

Changes

LID CompactString migration and encryption & history refactors

Layer / File(s) Summary
LID trait contract and cache return type
wacore/src/client/context.rs, src/lid_pn_cache.rs
SendContextResolver::get_lid_for_phone now returns Option<wacore_binary::CompactString>; LidPnCache::get_current_lid updated to return Option<CompactString> with import and test adjustments.
LID client resolution and propagation
src/client/context_impl.rs, src/client/device_registry.rs, src/client/lid_pn.rs, src/features/groups.rs, src/handlers/notification.rs, src/send.rs
Client-side callers and JID construction now accept and propagate CompactString values directly; removed several .into() conversions.
IsOnWhatsAppUser schema and usync plumbing
wacore/src/iq/usync.rs
IsOnWhatsAppUser::known_lid changed to Option<CompactString>; <lid> emission uses lid.as_str(); tests updated.
Message tests for LID type changes
src/message.rs
Tests updated to assert get_current_lid(...).await.as_deref() and to construct Jid::user using the compact-string value directly.
Per-device encryption optimization and helper extraction
wacore/src/send.rs
Added encrypt_one_device and push_encrypt_result; encrypt_for_devices gains single-device inline fast path and preserves bounded concurrent fan-out; per-device result shape adjusted; mock resolver updated.
History sync pre-sizing removal
wacore/src/history_sync.rs
Removed pre-counting and pre-sizing for msg_secret_records; deleted counting helpers and rely on demand-driven Vec growth.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the three main performance improvements (LID resolution, single-device send, history-sync) with measurable impact, directly aligned with the changeset.
Description check ✅ Passed The description comprehensively explains all three independent optimizations (M1, C3, C5), their technical rationale, performance measurements, breaking changes, and test coverage—fully related to the changeset.
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/lid-send-history-cleanups

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.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

Benchmark Results

67 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 2,838 2,838 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 8,272 8,272 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 31,317 31,317 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 13,827 13,827 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 49,398 49,398 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 54,827 54,827 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 1,592 1,592 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 4,219 4,219 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 112,785 113,049 -0.2%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,119 1,656,119 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 650,321 649,139 +0.2%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 876,015 874,656 +0.2%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 2,081,902 2,080,509 +0.1%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 747,516 746,910 +0.1%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,330,919 1,336,194 -0.4%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,375,727 4,378,969 -0.1%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 516,857 519,568 -0.5%
binary_benchmark::marshal_group::bench_marshal_allocating 45,401 45,401 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 45,451 45,451 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 66,354 66,354 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 43,512 43,512 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 45,507 45,507 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 4,930 4,930 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 4,961 4,961 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 6,732 6,732 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 528,529 528,529 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 528,150 528,150 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 529,396 529,396 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 5,417,786 5,417,786 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 5,362,043 5,362,043 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 13,276,336 13,276,336 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 1,850 1,850 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 29,217 29,217 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 618 618 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 672,890 672,890 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 3,736 3,736 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 3,840 3,840 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 48,283 48,283 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 3,866 3,866 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 48,344 48,344 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 5,206 5,206 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 66,668 66,668 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 310,312 310,312 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 8,286 8,286 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 254 254 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 91 91 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 292 292 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 137 137 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 317 317 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 145 145 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 27,425 27,425 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 10,725 10,725 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 4,142,605 4,141,162 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,133 100,131 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 4,263,702 4,263,702 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 99,803 99,803 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 210,262 210,249 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 496,921 496,921 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 508,474 510,381 -0.4%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,974,406 11,978,893 -0.0%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 2,466,138 2,466,138 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 4,921,242 4,913,562 +0.2%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,043,397 2,043,397 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 37,950 37,950 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 3,616,076 3,616,036 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 223,150 223,150 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 9,980,959 9,980,959 +0.0%
No significant changes detected.

@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/send.rs`:
- Around line 507-530: encrypt_one_device is currently cloning and storing
mediatype into every per-device EncryptOneResult, causing unnecessary heap
allocations; remove mediatype from EncryptOneResult and from the
encrypt_one_device signature/return so per-device results only contain enc_type,
is_prekey, ciphertext, and hide_decrypt_fail; update callers (the multi-device
fan-out that awaits encrypt_one_device tasks and the aggregation code that
builds the final <enc> element) to pass or use a single batch-level mediatype
when constructing the <enc> payload instead of relying on per-device results;
ensure functions referenced here (encrypt_one_device, EncryptOneResult,
message_encrypt, extract_ciphertext and the aggregation/fan-out code that
consumes EncryptOneResult) are adjusted accordingly and no longer clone
mediatype per device.
🪄 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: fd8896c9-85be-4b28-8163-05adcc4aac14

📥 Commits

Reviewing files that changed from the base of the PR and between 68d565d and 4f9d9f7.

📒 Files selected for processing (12)
  • src/client/context_impl.rs
  • src/client/device_registry.rs
  • src/client/lid_pn.rs
  • src/features/groups.rs
  • src/handlers/notification.rs
  • src/lid_pn_cache.rs
  • src/message.rs
  • src/send.rs
  • wacore/src/client/context.rs
  • wacore/src/history_sync.rs
  • wacore/src/iq/usync.rs
  • wacore/src/send.rs

Comment thread wacore/src/send.rs
mediatype is the same &'static str for every device in a send; it was cloned into each per-device EncryptOneResult (and, on the parallel path, cloned again per task). Drop it from EncryptOneResult and encrypt_one_device; thread the batch-level Option<&str> straight into push_encrypt_result when building the <enc> node.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wacore/src/send.rs (1)

523-528: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider zero-copy conversion from Box<[u8]> to Vec<u8>.

Look, this PR is all about cutting allocations, right? But here we're using .to_vec() which deref-coerces Box<[u8]> to &[u8] and then copies the bytes. For a perf-focused change, that's leaving gains on the table.

Vec::from(box) or .into() leverages From<Box<[T]>> for Vec<T> which reuses the allocation — zero copy.

♻️ Proposed fix for zero-copy conversion
             (
                 device_jid,
                 Ok(Some(EncryptOneResult {
                     enc_type,
                     is_prekey,
-                    ciphertext: serialized_bytes.to_vec(),
+                    ciphertext: serialized_bytes.into(),
                     hide_decrypt_fail,
                 })),
             )
🤖 Prompt for 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.

In `@wacore/src/send.rs` around lines 523 - 528, The code copies the boxed byte
slice when constructing EncryptOneResult.ciphertext via
serialized_bytes.to_vec(), which defeats the PR's allocation-reduction goal;
replace the copy with a zero-copy conversion using Vec::from(serialized_bytes)
or serialized_bytes.into() so the Box<[u8]> allocation is reused when building
the EncryptOneResult.ciphertext (ensure serialized_bytes is a Box<[u8]> or
convert it to one before the change).
🤖 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.

Outside diff comments:
In `@wacore/src/send.rs`:
- Around line 523-528: The code copies the boxed byte slice when constructing
EncryptOneResult.ciphertext via serialized_bytes.to_vec(), which defeats the
PR's allocation-reduction goal; replace the copy with a zero-copy conversion
using Vec::from(serialized_bytes) or serialized_bytes.into() so the Box<[u8]>
allocation is reused when building the EncryptOneResult.ciphertext (ensure
serialized_bytes is a Box<[u8]> or convert it to one before the change).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 38a0f6d3-8844-40e4-b731-d72908835887

📥 Commits

Reviewing files that changed from the base of the PR and between 4f9d9f7 and 23d3b8b.

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

extract_ciphertext returns Box<[u8]>; serialized_bytes.to_vec() copied it into a fresh Vec. Vec::from(Box<[u8]>) (via .into()) reuses the existing allocation, so the per-device ciphertext no longer pays an extra copy.
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