Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions wacore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,9 @@ harness = false
name = "message_utils_benchmark"
harness = false

[[bench]]
name = "appstate_sync_benchmark"
harness = false

[lints]
workspace = true
47 changes: 47 additions & 0 deletions wacore/benches/appstate_sync_benchmark.rs
Original file line number Diff line number Diff line change
@@ -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<wa::SyncdMutation> {
(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<u8> 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))));
}
38 changes: 35 additions & 3 deletions wacore/benches/message_utils_benchmark.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down
41 changes: 20 additions & 21 deletions wacore/src/appstate_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>> {
let mut out: Vec<Vec<u8>> = 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<String, Arc<ExpandedAppStateKeys>>,
key_id: &[u8],
Expand Down Expand Up @@ -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<u8>> = 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).
Expand Down Expand Up @@ -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<u8>> = 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<u8>, Vec<u8>> = self
.backend
.get_mutation_macs(collection_name, &need_db_lookup)
Expand Down
Loading