From 13c70e72c3f92d9f2f10129706c780a0588514c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Thu, 4 Jun 2026 01:32:40 -0300 Subject: [PATCH] perf(device-registry): cache Arc to avoid deep clone 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: 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. --- src/client.rs | 3 ++- src/client/device_registry.rs | 48 +++++++++++++++++++++++++++-------- src/handlers/notification.rs | 2 +- src/send.rs | 2 +- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/client.rs b/src/client.rs index 433db05cd..93fa3d99c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -506,7 +506,8 @@ pub struct Client { /// LRU cache for device registry (matches WhatsApp Web's 5000 entry limit). /// Maps user ID to DeviceListRecord for fast device existence checks. /// Backed by persistent storage. - pub(crate) device_registry_cache: TypedCache, + pub(crate) device_registry_cache: + TypedCache>, /// Router for dispatching stanzas to their appropriate handlers pub(crate) stanza_router: crate::handlers::router::StanzaRouter, diff --git a/src/client/device_registry.rs b/src/client/device_registry.rs index 4eb683e94..6d99fad51 100644 --- a/src/client/device_registry.rs +++ b/src/client/device_registry.rs @@ -5,6 +5,7 @@ use anyhow::Result; use log::{debug, info, warn}; +use std::sync::Arc; use wacore_binary::Jid; use super::Client; @@ -132,7 +133,7 @@ impl Client { // in the backend), not lookup_keys[0] which is our guessed canonical key. // This ensures consistency between the in-memory cache and the backend. self.device_registry_cache - .insert(record.user.clone(), record) + .insert(record.user.clone(), Arc::new(record)) .await; return has_device; } @@ -164,7 +165,7 @@ impl Client { // Use canonical_key directly as cache key (no extra clone) self.device_registry_cache - .insert(canonical_key.clone(), record_for_cache) + .insert(canonical_key.clone(), Arc::new(record_for_cache)) .await; let backend = self.persistence_manager.backend(); @@ -222,7 +223,7 @@ impl Client { let record_for_cache = record.clone(); self.device_registry_cache - .insert(canonical_key.clone(), record_for_cache) + .insert(canonical_key.clone(), Arc::new(record_for_cache)) .await; if canonical_key != original_user { @@ -532,7 +533,8 @@ impl Client { for key in lookup.all_keys() { if let Some(record) = self.device_registry_cache.get(key).await { - return Some(record); + // Cold load-modify-persist path: callers mutate the owned record. + return Some((*record).clone()); } } @@ -541,7 +543,7 @@ impl Client { match backend.get_devices(key).await { Ok(Some(record)) => { self.device_registry_cache - .insert(record.user.clone(), record.clone()) + .insert(record.user.clone(), Arc::new(record.clone())) .await; return Some(record); } @@ -583,7 +585,7 @@ impl Client { Ok(Some(record)) => { let devices = Self::reconstruct_device_jids(jid, &record); self.device_registry_cache - .insert(record.user.clone(), record) + .insert(record.user.clone(), Arc::new(record)) .await; return Some(devices); } @@ -658,7 +660,7 @@ impl Client { } self.device_registry_cache - .insert(lid.to_string(), record) + .insert(lid.to_string(), Arc::new(record)) .await; // Drop the PN-keyed row in both cache and DB. Invalidate @@ -713,10 +715,31 @@ mod tests { }; client .device_registry_cache - .insert(user.into(), record) + .insert(user.into(), Arc::new(record)) .await; } + #[tokio::test] + async fn warm_registry_hit_shares_arc_not_deep_clone() { + let client = create_test_client().await; + setup_device_record(&client, "15551112222", &[1, 2]).await; + + let a = client + .device_registry_cache + .get("15551112222") + .await + .expect("warm hit"); + let b = client + .device_registry_cache + .get("15551112222") + .await + .expect("warm hit"); + + // A warm registry hit returns a refcount bump of the same allocation, not a deep copy. + assert!(Arc::ptr_eq(&a, &b)); + assert_eq!(a.devices.len(), 2); + } + #[tokio::test] async fn test_resolve_to_canonical_key_unknown_user() { let client = create_test_client().await; @@ -986,7 +1009,7 @@ mod tests { }; client .device_registry_cache - .insert("15551234567".to_string(), record) + .insert("15551234567".to_string(), Arc::new(record)) .await; // Patch: update device 3 key_index to 5 @@ -1288,7 +1311,7 @@ mod tests { }; client .device_registry_cache - .insert("15551234567".into(), record) + .insert("15551234567".into(), Arc::new(record)) .await; // Warm the sender key device cache @@ -1541,7 +1564,10 @@ mod tests { backend.update_device_list(legacy.clone()).await.unwrap(); // Warm cache under PN to simulate a reader that populated it before // the mapping was learned. - client.device_registry_cache.insert(pn.into(), legacy).await; + client + .device_registry_cache + .insert(pn.into(), Arc::new(legacy)) + .await; setup_lid_pn(&client, lid, pn).await; diff --git a/src/handlers/notification.rs b/src/handlers/notification.rs index 59df6ac7c..597edd405 100755 --- a/src/handlers/notification.rs +++ b/src/handlers/notification.rs @@ -2159,7 +2159,7 @@ mod tests { }; client .device_registry_cache - .insert("5511999999999".into(), record) + .insert("5511999999999".into(), Arc::new(record)) .await; // Simulate identity change notification: type="encrypt" with child diff --git a/src/send.rs b/src/send.rs index 83aa0dc92..b91797367 100644 --- a/src/send.rs +++ b/src/send.rs @@ -2634,7 +2634,7 @@ mod tests { }; client .device_registry_cache - .insert((*user).into(), record) + .insert((*user).into(), Arc::new(record)) .await; }