Skip to content

perf(groups): cache Arc<GroupInfo> to avoid deep-cloning group metadata on warm sends - #710

Merged
jlucaso1 merged 1 commit into
mainfrom
perf/group-cache-arc
Jun 4, 2026
Merged

jlucaso1 merged 1 commit into
mainfrom
perf/group-cache-arc

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

The group metadata cache is a TypedCache<Jid, GroupInfo> (by value). query_info runs on every group send and, on a warm hit, returns Option<GroupInfo> by .clone() (both the moka and the portable backend clone the stored value). So each warm group send deep-clones the entire GroupInfo: the participants: Vec<Jid> plus the two HashMap<CompactString, Jid> LID/PN maps. For a large group that is hundreds of Jids and up to ~2x as many HashMap entries materialized and dropped per message sent.

This mirrors an anti-pattern the codebase already avoids elsewhere: SenderKeyDeviceCache (Arc<SenderKeyDeviceMap>) and lid_pn_cache (Arc<LidPnEntry>) both cache under Arc. group_cache was the exception caching by value.

Change

Cache Arc<GroupInfo> instead of GroupInfo. A warm query_info hit is now a refcount bump rather than a deep copy. This matches WA Web's WAWebGroupMetadataCollection, which keeps group metadata as a shared model passed by reference rather than copying the participant array per send.

  • query_info / resolve_group_info now return Arc<GroupInfo>.
  • prepare_group_stanza now takes &GroupInfo instead of &mut GroupInfo. It no longer mutates the (now shared) metadata to append our own JID; the force_skdm device-resolution branch already re-ensures self in its resolve list independently, so the push was redundant for the function's own reads.
  • Callers guarantee self is present before the reads that need it:
    • Group send path: a small helper rebuilds the Arc only when self is missing (rare, since the server's participant list already includes us), so the common warm path shares the metadata with no clone. Ordering relative to resolve_skdm_targets is preserved.
    • Status broadcast path: self is appended to the freshly-built (owned, non-shared) GroupInfo at the same relative position as before (after SKDM resolution, before stanza build), so ensure_status_participants behavior is unchanged.
  • Membership-change paths (participant add/remove notifications and the add/remove APIs) clone-on-write via Arc::unwrap_or_clone then re-insert. These are infrequent and off the hot send path.

The persisted group-metadata blob format is unchanged (the owned GroupInfo is serialized before being wrapped in Arc).

Benchmark

iai-callgrind, cloning a 256-participant LID GroupInfo (the per-warm-send work that is eliminated) vs cloning the Arc:

Operation Instructions Estimated cycles RAM hits
Deep-clone GroupInfo (before) 51,027 111,038 808
Arc clone (after) 20 102 2

~2,550x fewer instructions for the metadata copy per warm send, scaling with group size. Warm group-send latency is server-bound, so the win is allocation/CPU churn, not wall-clock.

Tests

  • warm_group_cache_hit_shares_arc_not_deep_clone: two gets on the same key return the same allocation (Arc::ptr_eq).
  • ensure_self_in_group_shares_when_present_and_appends_when_absent: when self is already a member the shared Arc passes through untouched (no clone); when missing, a fresh GroupInfo is built with self appended.
  • Full lib suites green (wacore + whatsapp-rust). cargo clippy --all-targets -- -D warnings clean.

Breaking

query_info, resolve_group_info, and prepare_group_stanza change signatures (return Arc<GroupInfo> / take &GroupInfo). Pre-1.0, in line with eliminating clones via signature changes.

@coderabbitai

coderabbitai Bot commented Jun 4, 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: 3acc0d84-7fa9-4605-ad13-cb8a15caa1a6

📥 Commits

Reviewing files that changed from the base of the PR and between d2a6e93 and 20dec38.

📒 Files selected for processing (8)
  • src/client.rs
  • src/client/context_impl.rs
  • src/features/groups.rs
  • src/handlers/notification.rs
  • src/send.rs
  • wacore/benches/send_receive_benchmark.rs
  • wacore/src/client/context.rs
  • wacore/src/send.rs

📝 Walkthrough

Summary by CodeRabbit

  • Refactor

    • Optimized group metadata storage to reduce memory overhead by sharing cached data more efficiently across the system.
  • Tests

    • Added tests to verify memory optimization behavior for cached group data.

Walkthrough

This PR refactors group metadata caching to use Arc<GroupInfo> instead of direct ownership, enabling shared references across the client without repeated clones. The trait contract, cache infrastructure, group query APIs, and group send paths are all updated to propagate and utilize these arc-wrapped references. Tests verify pointer-equality sharing behavior.

Changes

Arc-based Group Metadata Reference Counting

Layer / File(s) Summary
Trait Contract Foundation
wacore/src/client/context.rs
SendContextResolver::resolve_group_info now returns Result<Arc<GroupInfo>> instead of Result<GroupInfo>, establishing the contract all implementations must follow.
Client Cache Infrastructure
src/client.rs
GroupCache type alias introduced as TypedCache<Jid, Arc<GroupInfo>>, and the Client struct's group_cache field and get_group_cache method updated to work with arc-wrapped cache values.
SendContextResolver Client Implementation
src/client/context_impl.rs
Client's trait implementation of resolve_group_info updated to return Arc<GroupInfo> to satisfy the new contract.
Groups Query API and Arc Wrapping
src/features/groups.rs
Groups::query_info returns Arc<GroupInfo>, both cached and fetched paths wrap results in Arc, and a new test verifies warm cache hits share the same Arc allocation via Arc::ptr_eq.
Group Cache Mutation Semantics
src/features/groups.rs
add_participants and remove_participants now use Arc::unwrap_or_clone to obtain mutable values from the arc-wrapped cache without forcing unconditional clones, then re-insert updated state as fresh Arc values.
Notification Handler Cache Patching
src/handlers/notification.rs
w:gp2 Add and Remove participant actions now unwrap/clone cached group info via Arc::unwrap_or_clone, mutate the participant list, and re-insert Arc::new(info) into the cache.
Group Send Path and Immutable prepare_group_stanza
src/send.rs
New ensure_self_in_group helper returns the same Arc when sender is present or a cloned arc when self must be appended; send_status_message and group send in send_message_impl use this to augment participants without in-place mutation; all prepare_group_stanza calls pass immutable &group_info. Includes unit test validating Arc sharing and participant augmentation.
prepare_group_stanza Signature Update
wacore/src/send.rs
prepare_group_stanza now takes immutable &GroupInfo instead of &mut GroupInfo, removes internal participant mutation logic, and documents that the caller guarantees the sender's base JID is already in the participant list.
Benchmark and Test Integration
wacore/benches/send_receive_benchmark.rs, wacore/src/send.rs
MockResolver and MockSendContextResolver return Arc<GroupInfo>; benchmark group send/receive setup uses immutable prepare_group_stanza calls; test bindings remove unnecessary mut qualifiers; helper trait imports added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#382: Both PRs modify the group participant add/remove cache update paths in src/features/groups.rs and src/handlers/notification.rs, with this PR changing those patches to operate on Arc<GroupInfo> while #382 changes them to patch cached state instead of invalidating.
  • oxidezap/whatsapp-rust#545: Both PRs touch prepare_group_stanza in wacore/src/send.rs—this PR changes its signature to use immutable &GroupInfo, while #545 optimizes internal SKDM recipient filtering by borrowing own_sending_jid.user.
  • oxidezap/whatsapp-rust#568: Both PRs modify the status-sending path in src/send.rs around send_status_message and participant/own-LID anchoring before calling prepare_group_stanza.

Suggested labels

breaking-change, api-design

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: optimizing group metadata caching by using Arc to avoid deep cloning, which is the core performance improvement across the entire changeset.
Description check ✅ Passed The description comprehensively explains the problem, solution, implementation details, benchmarks, and test coverage. It directly relates to all changes in the PR and demonstrates clear understanding of the optimization.
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/group-cache-arc

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 4, 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() 113,081 112,836 +0.2%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,128 1,656,233 -0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 650,255 650,024 +0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 874,169 873,639 +0.1%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 2,081,874 2,081,238 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 746,710 746,652 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,328,457 1,330,445 -0.1%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,380,729 4,369,784 +0.3%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 517,219 515,893 +0.3%
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,133,413 4,141,001 -0.2%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,133 100,133 +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,262 +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,198 507,501 +0.1%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,976,458 11,979,816 -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,622 4,919,802 +0.0%
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,140 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.

@jlucaso1
jlucaso1 merged commit c41024c into main Jun 4, 2026
11 checks passed
@jlucaso1
jlucaso1 deleted the perf/group-cache-arc branch June 4, 2026 12:36
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