From a179b514122e02b773dfc7b028e242081389b91f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 02:05:30 +0000 Subject: [PATCH] =?UTF-8?q?bench:=20cover=20the=20receive=20path=20?= =?UTF-8?q?=E2=80=94=20plaintext=20decode=20and=20appstate=20index-MAC=20d?= =?UTF-8?q?edup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps on the inbound side, benched before any optimization: - decode_plaintext (unpad + prost decode): runs on every received message and is the inbound mirror of the already-benched encode_and_pad. Shapes match the send-side bench plus an inline-SKDM group first-message. - collect_unique_index_macs: the O(N²) linear-scan dedup feeding the batched previous-value-MAC lookup, extracted from its two inline copies (inbound process_patch_list and outbound build_patch) into a pure function so it can be measured. At the 1000-mutation resume-sync upper bound the scan takes ~1.5ms; a HashSet swap measured 6-120% slower at small N in this codebase, so both ends are pinned (N=10 and N=1000) before deciding. No behavior change: the extraction is byte-identical logic. https://claude.ai/code/session_01EoJjbyorpCARCBTZSmUMRr --- wacore/Cargo.toml | 4 ++ wacore/benches/appstate_sync_benchmark.rs | 47 +++++++++++++++++++++++ wacore/benches/message_utils_benchmark.rs | 38 ++++++++++++++++-- wacore/src/appstate_sync.rs | 41 ++++++++++---------- 4 files changed, 106 insertions(+), 24 deletions(-) create mode 100644 wacore/benches/appstate_sync_benchmark.rs diff --git a/wacore/Cargo.toml b/wacore/Cargo.toml index a514fee06..1941deddf 100644 --- a/wacore/Cargo.toml +++ b/wacore/Cargo.toml @@ -88,5 +88,9 @@ harness = false name = "message_utils_benchmark" harness = false +[[bench]] +name = "appstate_sync_benchmark" +harness = false + [lints] workspace = true diff --git a/wacore/benches/appstate_sync_benchmark.rs b/wacore/benches/appstate_sync_benchmark.rs new file mode 100644 index 000000000..7dff285a2 --- /dev/null +++ b/wacore/benches/appstate_sync_benchmark.rs @@ -0,0 +1,47 @@ +//! App-state patch-list hot paths in the `wacore` orchestration layer: the +//! index-MAC dedup that feeds the batched previous-value-MAC lookup, run once +//! per inbound patch and per outbound build_patch. The linear scan is O(N²) +//! over distinct indices; HashSet measured slower at small N in this codebase, +//! so both ends are pinned here before any swap. + +use divan::black_box; +use wacore::appstate_sync::collect_unique_index_macs; +use waproto::whatsapp as wa; + +fn main() { + divan::main(); +} + +/// N SET mutations with distinct 32-byte index MACs — distinct indices are the +/// realistic patch shape and the scan's worst case (full compare per element). +fn setup_mutations(n: usize) -> Vec { + (0..n as u64) + .map(|i| { + let mut index_mac = vec![0u8; 32]; + index_mac[..8].copy_from_slice(&i.to_le_bytes()); + wa::SyncdMutation { + operation: Some(wa::syncd_mutation::SyncdOperation::Set as i32), + record: Some(wa::SyncdRecord { + index: Some(wa::SyncdIndex { + blob: Some(index_mac), + }), + value: Some(wa::SyncdValue { + blob: Some(vec![0x5A; 48]), + }), + key_id: Some(wa::KeyId { + id: Some(b"AAAA".to_vec()), + }), + }), + } + }) + .collect() +} + +/// 10 = a typical incremental patch; 1000 = the resume-sync upper bound, where +/// the quadratic scan does ~500k Vec compares. +#[divan::bench(args = [10, 1000])] +fn bench_collect_unique_index_macs(bencher: divan::Bencher, n: usize) { + bencher + .with_inputs(|| setup_mutations(n)) + .bench_refs(|mutations| black_box(collect_unique_index_macs(black_box(mutations)))); +} diff --git a/wacore/benches/message_utils_benchmark.rs b/wacore/benches/message_utils_benchmark.rs index 58ac99e73..8d16a9bc5 100644 --- a/wacore/benches/message_utils_benchmark.rs +++ b/wacore/benches/message_utils_benchmark.rs @@ -1,6 +1,7 @@ -//! Per-send message utilities on realistic shapes: the participant hash that -//! runs on every group send (1600 devices = a large LID group) and the -//! pad/encode steps every outgoing message pays before encryption. +//! Per-message utilities on realistic shapes: the participant hash that +//! runs on every group send (1600 devices = a large LID group), the +//! pad/encode steps every outgoing message pays before encryption, and the +//! unpad/decode steps every incoming message pays after decryption. use divan::black_box; use wacore::messages::MessageUtils; @@ -102,6 +103,37 @@ fn dm_shape(shape: &str) -> wa::Message { } } +fn recv_shape(shape: &str) -> wa::Message { + match shape { + // The first group message from a sender carries the SKDM inline + // alongside the content. + "group_skdm_text" => wa::Message { + sender_key_distribution_message: Some(wa::message::SenderKeyDistributionMessage { + group_id: Some("120363000000000001@g.us".into()), + axolotl_sender_key_distribution_message: Some(vec![0x33; 350]), + }), + conversation: Some("Benchmark group message with realistic text.".into()), + ..Default::default() + }, + other => dm_shape(other), + } +} + +/// Unpad + prost decode of a received padded plaintext: the pure tail of every +/// inbound message decryption, and the inbound mirror of `encode_and_pad`. +/// Shapes match the send-side bench plus the inline-SKDM group first-message. +#[divan::bench(args = ["text_reply", "media_refs", "large_text", "group_skdm_text"])] +fn bench_decode_plaintext(bencher: divan::Bencher, shape: &str) { + bencher + .with_inputs(|| { + use prost::Message as _; + MessageUtils::pad_message_v2(recv_shape(shape).encode_to_vec()) + }) + .bench_refs(|padded| { + black_box(wacore::messages::decode_plaintext(black_box(padded), 2).unwrap()) + }); +} + /// The CPU a single DM send pays in the encode/token department, mirroring /// `wacore::send::dm` plus the retry-cache serialization the client does: /// reporting token (full content encode + HKDF + HMAC), the splice into the diff --git a/wacore/src/appstate_sync.rs b/wacore/src/appstate_sync.rs index 1cc414e96..ecefa1404 100644 --- a/wacore/src/appstate_sync.rs +++ b/wacore/src/appstate_sync.rs @@ -23,6 +23,24 @@ use waproto::whatsapp as wa; // Re-export Mutation from appstate for convenience pub use crate::appstate::Mutation; +/// Unique index MACs of a patch's mutations, in first-seen order, feeding the +/// batched previous-value-MAC backend lookup. Linear-scan dedup is deliberate: +/// HashSet measured 6-120% slower at small N in this codebase, so don't switch +/// without benchmarking both ends (patches carry up to ~1000 mutations). +pub fn collect_unique_index_macs(mutations: &[wa::SyncdMutation]) -> Vec> { + let mut out: Vec> = Vec::with_capacity(mutations.len()); + for m in mutations { + if let Some(rec) = &m.record + && let Some(ind) = &rec.index + && let Some(index_mac) = &ind.blob + && !out.iter().any(|v| v == index_mac) + { + out.push(index_mac.clone()); + } + } + out +} + fn lookup_app_state_key( keys_map: &HashMap>, key_id: &[u8], @@ -380,17 +398,7 @@ impl AppStateProcessor { let patches = std::mem::take(&mut pl.patches); let mut processed_patches = Vec::with_capacity(patches.len()); for patch in patches { - // Collect index MACs we need to look up (pre-allocate with upper bound) - let mut need_db_lookup: Vec> = Vec::with_capacity(patch.mutations.len()); - for m in &patch.mutations { - if let Some(rec) = &m.record - && let Some(ind) = &rec.index - && let Some(index_mac) = &ind.blob - && !need_db_lookup.iter().any(|v| v == index_mac) - { - need_db_lookup.push(index_mac.clone()); - } - } + let need_db_lookup = collect_unique_index_macs(&patch.mutations); // Fetch previous value MACs in one backend round-trip instead of a // spawn_blocking + query per mutation (N+1). @@ -492,16 +500,7 @@ impl AppStateProcessor { // Pre-fetch previous value MACs in one backend round-trip, mirroring // the inbound patch path: one batched query instead of a // spawn_blocking + single-row SELECT per mutation. - let mut need_db_lookup: Vec> = Vec::with_capacity(mutations.len()); - for m in &mutations { - if let Some(rec) = &m.record - && let Some(ind) = &rec.index - && let Some(index_mac) = &ind.blob - && !need_db_lookup.iter().any(|v| v == index_mac) - { - need_db_lookup.push(index_mac.clone()); - } - } + let need_db_lookup = collect_unique_index_macs(&mutations); let db_prev: std::collections::HashMap, Vec> = self .backend .get_mutation_macs(collection_name, &need_db_lookup)