Skip to content

perf(send): hash the participant list from one arena instead of a String per device - #822

Merged
jlucaso1 merged 1 commit into
mainfrom
perf/phash-arena-sort
Jun 10, 2026
Merged

perf(send): hash the participant list from one arena instead of a String per device#822
jlucaso1 merged 1 commit into
mainfrom
perf/phash-arena-sort

Conversation

@jlucaso1

Copy link
Copy Markdown
Collaborator

Problem

participant_list_hash runs over the full device set on every group send (and the phash now travels on every send since #678). It materialized a Vec<String> with one to_ad_string() heap allocation per device and sorted the Strings: for an 800-device group that is 801 discarded allocations per message, pure allocator churn on the send hot path.

Change

Format every device into one shared arena String via a new Jid::push_ad_to (the append-style sibling of to_ad_string, which now delegates to it), and sort lightweight (start, end) range views over the arena. Sorting the slices is the same lexicographic order as sorting the individual Strings, so the hashed concatenation is byte-identical: the pinned cross-impl vectors (phash_crosscheck_vectors, locked against whatsmeow and WA Web server behavior) pass unchanged.

Benchmark

Counting-allocator measurement (release, 800 devices, 1000 iterations, bench not committed):

allocs per call
before (String per device + sort, hash excluded) 801
after (arena + ranges, full function incl. SHA-256 + base64) 3

Wall time of the full function is dominated by the SHA-256 over ~26KB and stays flat; the win is the 798 fewer allocations per group send.

Tests

New phash_arena_matches_per_string_reference: locks the arena output against an inline reference implementation (String per device, sorted, concatenated, hashed) over a mixed set with unsorted input, duplicates, agents, multiple servers, and the "111" vs "1110" prefix-ordering edge where a slice-comparator bug would diverge. The existing pinned vectors (phash_crosscheck_vectors) and validate_bcl_hash tests pass unchanged.

  • cargo fmt --all
  • cargo clippy --all-targets -- -D warnings
  • cargo test -p wacore (995 passing)
  • cargo test -p whatsapp-rust --lib (753 passing)
  • cargo test -p wacore-binary (98 passing)

Breaking

None. to_ad_string keeps its exact output; push_ad_to is additive.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Refactor

    • Enhanced internal JID string formatting implementation.
    • Optimized message hash computation.
  • Tests

    • Added verification test for device list hashing across various scenarios.

Walkthrough

The PR extracts Jid AD-string rendering into a reusable push_ad_to helper method, then refactors MessageUtils::participant_list_hash to eliminate per-device string allocations by building a single arena string, recording byte ranges, sorting by arena slice content, and feeding sorted slices to SHA-256 incrementally. A differential test verifies the arena implementation matches a straightforward reference approach.

Changes

AD String Optimization for Participant Hashing

Layer / File(s) Summary
Jid AD string builder helper
wacore/binary/src/jid.rs
A new pub fn push_ad_to(&self, buf: &mut String) method encapsulates AD-string formatting (server-only when user is empty, otherwise user.agent:device@server with numeric conversion via itoa). The existing to_ad_string method now allocates and delegates to this helper, replacing the previous inline logic and preserving the empty-user case.
Arena-based participant list hashing with verification
wacore/src/messages.rs
MessageUtils::participant_list_hash rewrites the phash computation to build a single arena String containing all device ad strings, records (start,end) byte ranges per device, sorts ranges by lexicographic order of their corresponding arena substrings, then feeds each sorted slice to SHA-256. The 2: prefix and base64 output format remain unchanged. A new test phash_arena_matches_per_string_reference verifies the arena+range sorting implementation against a reference implementation (per-device string sort + concatenation) across a mixed device set including duplicates and prefix-ordering edge cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#376: Overlaps in wacore/binary/src/jid.rs around AD string formatting in Jid::to_ad_string, where the retrieved PR adjusts formatting via Cow types while the main PR extracts a reusable helper.
  • oxidezap/whatsapp-rust#678: Modifies the same MessageUtils::participant_list_hash phash computation and base64 formatting in wacore/src/messages.rs, directly adjacent to the main PR's arena refactor.
  • oxidezap/whatsapp-rust#697: Changes MessageUtils::participant_list_hash implementation and to_ad_string() input handling in wacore/src/messages.rs, directly overlapping with the main PR's refactor of the same function and phash output construction.

Look, this is solid work. You're eliminating wasteful per-device allocations in the phash computation—that matters when you're dealing with real group scale. The arena pattern is clean: single buffer, byte ranges, sort semantically on the slices themselves, feed the hasher in sorted order. That's how you avoid garbage and keep things moving.

The push_ad_to helper is the right extraction. Jid formatting logic lives in one place now, which means no drift between to_ad_string and other callers. The test covering arena behavior against a reference implementation shows you're not guessing—you've verified the optimization doesn't break the math.

But make sure the arena string stays in scope for the entire sort-and-hash sequence. Any borrow issues hiding in there will bite you in production. And that test set with duplicates and prefix cases—keep that sharp. Edge cases in sort order are where correctness bugs hide.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main performance optimization: replacing per-device String allocations with a single arena-based approach for hashing participant lists.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the problem, solution, benchmarks, and test coverage with clear rationale for 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/phash-arena-sort

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

Copy link
Copy Markdown

Benchmark Results

2 improvement(s):

Benchmark Current Baseline Change
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,989,563 2,076,296 -4.2%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 852,545 870,330 -2.0%
65 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 2,927 2,927 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 8,448 8,448 +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,487 49,487 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 55,003 55,003 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 1,679 1,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 4,393 4,393 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 113,166 113,049 +0.1%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,730 1,656,622 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 641,491 644,372 -0.4%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 733,446 737,389 -0.5%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,284,181 1,304,945 -1.6%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,173,519 4,219,185 -1.1%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 516,171 520,020 -0.7%
binary_benchmark::marshal_group::bench_marshal_allocating 40,690 40,690 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 40,743 40,743 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 61,909 61,909 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 38,953 38,953 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 40,796 40,796 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 5,144 5,144 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 5,174 5,174 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 6,954 6,954 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 528,339 528,339 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 527,963 527,963 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 529,211 529,211 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 4,769,980 4,769,980 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 4,769,621 4,769,621 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 12,643,701 12,643,701 +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() 28,069 28,069 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 618 618 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 672,888 672,888 +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,845 3,845 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 47,180 47,180 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 3,871 3,871 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 47,241 47,241 +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() 65,610 65,610 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 234,591 234,591 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 8,579 8,579 +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,144,569 4,144,385 +0.0%
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,264,189 4,264,189 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 100,399 100,399 +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,435 511,983 -0.7%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,973,153 11,979,075 -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,895,902 4,877,892 +0.4%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,043,351 2,043,351 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 37,414 37,414 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 3,617,967 3,617,967 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 230,648 230,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 9,980,959 9,980,959 +0.0%

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/src/messages.rs`:
- Around line 977-990: The test currently only verifies the 6-byte
truncated/base64 "public phash" string; change it to also compute and assert the
full 32-byte SHA-256 digest before formatting: generate the full digest via
Sha256 (the variable digest produced from hasher.finalize()), compare that full
digest bytes (or hex) against the expected full-digest test vector, then
separately preserve the existing base64 "2:" formatting (expected string) and
assert MessageUtils::participant_list_hash(&devices) still matches the public
string form; update the test to split digest generation (hasher/digest) from the
formatting step (expected string) and add the full-digest equality assertion
against the canonical 32-byte value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1314e1e6-f864-427a-b10a-5b53a4b0b244

📥 Commits

Reviewing files that changed from the base of the PR and between b65fd0d and 5d17f87.

📒 Files selected for processing (2)
  • wacore/binary/src/jid.rs
  • wacore/src/messages.rs

Comment thread wacore/src/messages.rs
@jlucaso1
jlucaso1 merged commit 1162b13 into main Jun 10, 2026
16 checks passed
@jlucaso1
jlucaso1 deleted the perf/phash-arena-sort branch June 10, 2026 04:13
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