Skip to content

perf(device-registry): cache Arc<DeviceListRecord> to avoid deep clone on warm hits - #703

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

jlucaso1 merged 1 commit into
mainfrom
perf/device-registry-cache-arc

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

get_devices_from_registry and has_device read device_registry_cache (a moka-backed TypedCache) on the warm device-resolution path: once per recipient on every DM send, and once per participant on group fanout. moka's get returns the value by clone, so each cache hit deep-copies the whole DeviceListRecord (a String user + Vec<DeviceInfo> + Option<String> phash + ...) only for the callers to borrow it read-only (reconstruct_device_jids(jid, &record), record.devices.iter().any(...)). The deep clone is pure throwaway, on the cache-HIT path that runs per recipient / participant.

Change

Store the cache value as Arc<DeviceListRecord>. A warm hit now returns an Arc clone (a refcount bump) instead of deep-copying the record; the two hot borrow-only callers work unchanged via deref, and inserts wrap the record in Arc::new. serde's rc feature (already enabled) lets Arc<DeviceListRecord> satisfy the cache's Serialize / DeserializeOwned bounds used by the optional custom-store backend.

The one cold caller that needs an owned, mutable record (load_device_record, the load-modify-persist path) clones the inner value on a cache hit ((*arc).clone()). That re-introduces a clone, but only off the per-send hot path, so it stays a clear net win.

Tests

New warm_registry_hit_shares_arc_not_deep_clone: two warm gets of the same key return Arcs pointing to the same allocation (Arc::ptr_eq), proving the hit is a refcount bump rather than a deep copy.

cargo fmt --all
cargo clippy -p whatsapp-rust --all-targets -- -D warnings   # clean
cargo test -p whatsapp-rust --lib   # 669 pass (incl. 1 new)

Breaking

None. device_registry_cache is pub(crate); the public API and all observable behavior are unchanged.

…e on warm hits

get_devices_from_registry and has_device read device_registry_cache on the
warm device-resolution path (per recipient on DM send, per participant on
group fanout). moka's get clones the value, so each hit deep-copied the whole
DeviceListRecord only for the callers to borrow it read-only.

Store Arc<DeviceListRecord>: a warm hit is now a refcount bump. The borrow-only
callers are unchanged; load_device_record (cold load-modify-persist) clones the
inner value to keep returning an owned record.
@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: e626f839-e2d7-429c-b5bf-63c25c77fb58

📥 Commits

Reviewing files that changed from the base of the PR and between b75f523 and 13c70e7.

📒 Files selected for processing (4)
  • src/client.rs
  • src/client/device_registry.rs
  • src/handlers/notification.rs
  • src/send.rs

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Refactor
    • Internal optimization of device registry caching mechanism to improve memory efficiency.

Walkthrough

The device registry cache storage type is upgraded from owning DeviceListRecord values to storing Arc<DeviceListRecord>. All insertion points—cold-load paths, record updates, and migrations—wrap records with Arc::new(). The warm-hit read path dereferences and clones. Tests verify Arc sharing behavior and prepopulate caches with the correct wrapped type.

Changes

Device Registry Cache Arc Wrapping

Layer / File(s) Summary
Cache type contract
src/client.rs, src/client/device_registry.rs
Client::device_registry_cache field type changes from TypedCache<String, DeviceListRecord> to TypedCache<String, Arc<DeviceListRecord>>, and Arc is imported in the device registry module.
Cache insertion updates
src/client/device_registry.rs
All record insertions across has_device, update_device_list, update_device_lists, and cold-load paths (load_device_record, get_devices_from_registry, migrate_device_registry_on_lid_discovery) wrap records with Arc::new().
Cache read path
src/client/device_registry.rs
load_device_record warm-hit behavior dereferences and clones the cached Arc<DeviceListRecord> to return an owned copy.
Test setup and validation
src/client/device_registry.rs, src/handlers/notification.rs, src/send.rs
Helper setup_device_record wraps cache inserts in Arc::new(), a new test verifies warm cache hits use Arc pointer equality, and all test cases update cache prepopulation to match the wrapped type.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#489: Both PRs update cache prepopulation in test_identity_change_dispatches_event_and_invalidates_cache to wrap DeviceListRecord in Arc, ensuring device registry cache type consistency with identity-change handler invalidation.

  • oxidezap/whatsapp-rust#681: Both PRs modify get_devices_from_registry in src/client/device_registry.rs; this PR changes cache value storage to Arc<DeviceListRecord> and updates read/write paths accordingly.

  • oxidezap/whatsapp-rust#445: Both PRs affect sender-key device caching in src/send.rs tests; this PR updates the device registry cache type to Arc, making record sharing and warm-hit deref behavior compatible with SKDM target resolution.

Suggested reviewers

  • Ari4ka

Look, this is straightforward refactoring, but it needs to be airtight. The Arc wrapping pattern is applied uniformly across all cache insertion paths—cold-load paths, record updates, migrations, and test setup. Every single cache write goes through Arc now. The warm-hit read path gets the deref-and-clone semantics right so callers still get owned values without breaking expectations.

Tests validate the behavioral change explicitly: the new test verifies Arc pointer equality on warm hits, meaning allocations are actually shared, not deep-cloned. That's the whole point here—we're squeezing efficiency out of the cache layer by not duplicating records on every warm access.

The changes span multiple files—device registry, handlers, send module—but they're all mechanical: find the cache insert, wrap it in Arc::new(). Find the test setup, do the same. This type of consistency work either gets done right everywhere or it doesn't work at all. Check that every insertion point is covered and none were missed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely summarizes the main performance optimization: caching Arc to eliminate unnecessary deep clones on cache hits.
Description check ✅ Passed Description thoroughly explains the problem (unnecessary deep clones on cache hits), the solution (Arc wrapping), implementation details, test coverage, and confirms no breaking changes.
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/device-registry-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() 112,835 113,083 -0.2%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,234 1,656,232 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 650,240 650,178 +0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 873,751 873,761 -0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 2,081,612 2,081,591 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 747,433 747,253 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,330,543 1,326,397 +0.3%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,369,666 4,393,099 -0.5%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 517,085 519,911 -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,140,170 4,138,544 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,131 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,249 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() 510,099 510,136 -0.0%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,974,589 11,978,068 -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,894,622 4,929,252 -0.7%
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.

@jlucaso1
jlucaso1 merged commit 93bb7fe into main Jun 4, 2026
11 checks passed
@jlucaso1
jlucaso1 deleted the perf/device-registry-cache-arc branch June 4, 2026 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant