Skip to content

perf: single-buffer ProtocolAddress + reusable hot-loop address construction - #518

Merged
jlucaso1 merged 2 commits into
mainfrom
perf/protocol-address-reuse
Apr 12, 2026
Merged

perf: single-buffer ProtocolAddress + reusable hot-loop address construction#518
jlucaso1 merged 2 commits into
mainfrom
perf/protocol-address-reuse

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Single-buffer ProtocolAddress

Collapsed ProtocolAddress from two Strings (name + display) into one (buf + name_len). The buffer stores "{name}.{device_id}" and accessors are zero-cost slices:

  • name()&buf[..name_len]
  • as_str()&buf
Before (2 strings) After (1 buffer)
new(name, device_id) 2 allocs (name + format! display) 1 alloc (take name, append suffix)
reset_with() write name → copy to display → append write name → append. No copy.
clone() clone 2 Strings clone 1 String
Struct size ~56 bytes ~40 bytes

All 40+ cold-path to_protocol_address() call sites benefit automatically.

DRY address formatting

  • write_signal_address_to() is the single core helper — all address functions delegate to it
  • append_device_suffix() shared by new, reset_with — fast path for device_id=0 (push_str(".0") instead of write!)
  • make_address_buffer() / make_reusable_protocol_address() factories — no magic numbers
  • SIGNAL_ADDRESS_CAPACITY (64) and SIGNAL_DEVICE_ID constants replace magic values
  • DEFAULT_USER_SERVER / LEGACY_USER_SERVER replace magic strings in mapped_server()

Hot loop optimization

In encrypt_for_devices(), one reusable ProtocolAddress across all 4 per-device loops:

  1. LID session check
  2. Direct session check
  3. Prekey processing
  4. Message encryption

reset_with(|name| write_signal_address_to(jid, name)) writes directly into the buffer — single write pass, no intermediate copy. For a 100-device group, eliminates hundreds of String allocations per send.

Buffer capacity

64 bytes based on real WhatsApp logs (docs/real-whatsapp-log.json) showing max signal address length of 53 chars.

Lab results (experiments/heaptrack-impact-lab)

Scenario Alloc reduction Bytes reduction Time reduction
Reusable buffer (unique fanout) -100% -100% -78.9%

Test plan

  • cargo fmt --all
  • cargo clippy --all --tests
  • cargo test --all --exclude e2e-tests
  • test_reset_protocol_address_matches_fresh — hard-coded expected strings including dotted LID
  • test_write_functions_dry — hard-coded expected output, not self-referential
  • test_protocol_address_string_matches_to_string — dotted LID, phone, device variants

Summary by CodeRabbit

  • Refactor
    • Improved protocol address handling for lower memory use and faster reuse.
    • Consolidated address normalization to rely on shared configuration for consistent formatting.
  • Tests
    • Updated address-related tests for the new in-place formatting behavior.
  • Chores
    • Tightened benchmark regression threshold for CI reporting (more sensitive detection).

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c428d438-191a-49a2-aca3-5006e1f773a4

📥 Commits

Reviewing files that changed from the base of the PR and between ed0b21a and 5e7a4f0.

📒 Files selected for processing (1)
  • .github/scripts/bench-comment.py

📝 Walkthrough

Walkthrough

Replaced per-iteration address allocations with preallocated buffers and in-place ProtocolAddress mutation; updated JID/address helpers and switched session lookup, prekey handling, identity saving, and per-device encryption/logging to reuse the same normalized address buffer.

Changes

Cohort / File(s) Summary
Client session locking
src/send.rs
Replaced ephemeral String::with_capacity(64) with wacore::types::jid::make_address_buffer() when constructing session lock keys in Client::session_mutexes_for.
Send path encryption
wacore/src/send.rs
Introduced a reusable ProtocolAddress (reusable_addr) and switched session lookup, prekey processing, identity saving, encryption calls, and related logs to reset_protocol_address(&mut reusable_addr) / pass &reusable_addr.
ProtocolAddress internals
wacore/libsignal/src/core/address.rs
Refactored ProtocolAddress to store a single buf + name_len; added DeviceId::new, ProtocolAddress::with_capacity, reset_with, and helpers for digit sizing and suffix appending to support in-place mutation and preallocation.
JID formatting & helpers
wacore/src/types/jid.rs
Added make_address_buffer() and make_reusable_protocol_address(), split formatting into write_signal_address_to() / write_protocol_address_to(), added reset_protocol_address() on JidExt, and refactored callers/tests to use the buffer/write APIs and server/device constants.
Benchmark script
.github/scripts/bench-comment.py
Lowered regression detection threshold constant from 0.05 to 0.02 and adjusted comment string construction formatting.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ReusableAddr as ReusableAddrBuf
    participant SessionStore
    participant PrekeyProcessor
    participant IdentityStore
    participant Encryptor

    Client->>ReusableAddr: make_reusable_protocol_address() / reset_protocol_address(&reusable_addr)
    Client->>SessionStore: load_session(&reusable_addr)
    alt session exists
        SessionStore-->>Client: session
    else session missing
        Client->>PrekeyProcessor: process_prekey_bundle(&reusable_addr, ...)
        PrekeyProcessor->>IdentityStore: save_identity(&reusable_addr, ...)
        PrekeyProcessor-->>Client: new_session
    end
    Client->>Encryptor: message_encrypt(&reusable_addr, ...)
    Encryptor-->>Client: encrypted_message / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Ari4ka

Poem

🐇 I nibble buffers, stitch one line,
Reset the name, append the sign.
One reusable string for every hop,
I keep things tidy—no more pop! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main performance optimization: refactoring ProtocolAddress to use a single buffer and introducing reusable address construction in hot loops.
Docstring Coverage ✅ Passed Docstring coverage is 88.24% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/protocol-address-reuse

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.

@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Apr 12, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 2m 2s —— View job


Code Review: perf: reusable ProtocolAddress in encrypt_for_devices hot loops

  • Read changed files and diff
  • Review wacore/libsignal/src/core/address.rs
  • Review wacore/src/types/jid.rs
  • Review wacore/src/send.rs
  • Review src/send.rs
  • Post review feedback

Overall this is a well-targeted optimization with clean factoring. The write_signal_address_to extraction and delegation pattern is solid, and replacing magic strings with named constants is a nice cleanup. A few items worth addressing:

Issues

1. display field not pre-allocated in make_reusable_protocol_address()wacore/src/types/jid.rs:26-34

ProtocolAddress::new(String::with_capacity(64), SIGNAL_DEVICE_ID) passes an empty name to new(), which calls format!("{name}.{device_id}") producing ".0" — a string with ~2 bytes capacity. The first reset_name() call will reallocate display to fit the full address. This means the first loop iteration is not zero-alloc as the doc comment implies.

Consider adding a dedicated constructor that pre-allocates both name and display, or document that the first call does a one-time realloc.

Fix this →

2. Unused ProtocolAddress allocation in src/send.rs:1518

let (mut buf, _) = wacore::types::jid::make_reusable_protocol_address();

This call site only needs the String buffer for lock key lookup but discards the ProtocolAddress, wasting 2 heap allocations (name + display strings). Since SIGNAL_ADDRESS_CAPACITY is private, this is the only way to get a pre-sized buffer. Consider either:

  • Making SIGNAL_ADDRESS_CAPACITY public, or
  • Adding a make_reusable_address_buffer() -> String helper

Fix this →

3. Removed test coverage for dotted LID addresses

The old test suite included "100000000000001.1:75@lid" — an important edge case since LID user IDs can contain dots that could be confused with the .{device_id} separator in ProtocolAddress::display. This JID was tested in test_signal_address_string_lid_with_dot_in_user, test_protocol_address_lid_with_dot, and test_protocol_address_string_matches_to_string. All three were removed.

I'd recommend adding it back to at least test_protocol_address_string_matches_to_string and test_reset_protocol_address_matches_fresh to ensure the reusable path handles dots correctly.

Fix this →

Nits

4. reset_name can avoid fmt::Write for constant device_idwacore/libsignal/src/core/address.rs:312-322

Since SIGNAL_DEVICE_ID is always 0, reset_name could use self.display.push('0') instead of pulling in fmt::Write. However, reset_name is a general-purpose method on ProtocolAddress and doesn't know the device_id is always 0, so the current approach is defensively correct. Just noting it as a possible micro-optimization if this method is only ever used in this context.

5. to_protocol_address_string double-computes mapped_serverwacore/src/types/jid.rs:107-112

fn to_protocol_address_string(&self) -> String {
    let server = mapped_server(self.server.as_str());  // first call
    let mut buf = String::with_capacity(self.user.len() + 9 + server.len());
    write_protocol_address_to(self, &mut buf);  // calls mapped_server again inside
    buf
}

mapped_server is called once for the capacity hint, then again inside write_signal_address_to. Trivial cost since it's just a string comparison, but if you wanted to clean it up, you could pass the pre-computed server into the write function or just use SIGNAL_ADDRESS_CAPACITY for the hint.

Summary

The optimization strategy is sound — reusing one (String, ProtocolAddress) pair across the 4 per-device loops in encrypt_for_devices is a clean win. The refactoring of address helpers into a delegation chain with write_signal_address_to at the core is well done.

The main actionable items are #2 (wasted allocation at the session_mutexes_for call site) and #3 (restore dotted-LID test coverage). Item #1 is worth considering but not blocking.


@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/send.rs`:
- Around line 1518-1521: The code is allocating a full ProtocolAddress via
make_reusable_protocol_address() and then calling write_protocol_address_to(),
which builds the ProtocolAddress and its cached display unnecessarily for
session_lock_for()/session_mutexes_for(); change to a string-only helper that
returns or writes just the lock-key string (e.g.,
make_reusable_protocol_lock_key or write_protocol_address_key_to) and use that
buffer when calling session_lock_for(&buf). Replace calls to
make_reusable_protocol_address() + write_protocol_address_to() with the new
string-only helper so you avoid constructing ProtocolAddress and its cached
display on the hot path.

In `@wacore/src/types/jid.rs`:
- Around line 23-34: make_reusable_protocol_address() still causes a
reallocation because ProtocolAddress::new builds its internal display from an
empty name sized only for ".0"; add a capacity-aware constructor on
ProtocolAddress (e.g., ProtocolAddress::with_capacity or new_with_capacity) that
preallocates the internal display/string to the expected full size (derive
capacity from SIGNAL_ADDRESS_CAPACITY plus room for "." and SIGNAL_DEVICE_ID),
update ProtocolAddress::with_capacity(...) to initialize fields without growing
on first reset, and call that new constructor from
make_reusable_protocol_address() instead of ProtocolAddress::new so
reset_protocol_address() can avoid allocations on first use.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b88dd0df-a5d2-4545-b98a-581b26521e48

📥 Commits

Reviewing files that changed from the base of the PR and between d367c35 and ff58633.

📒 Files selected for processing (4)
  • src/send.rs
  • wacore/libsignal/src/core/address.rs
  • wacore/src/send.rs
  • wacore/src/types/jid.rs

Comment thread src/send.rs Outdated
Comment thread wacore/src/types/jid.rs Outdated
@github-actions

github-actions Bot commented Apr 12, 2026

Copy link
Copy Markdown

Benchmark Results

3 improvement(s):

Benchmark Current Baseline Change
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,464,707 1,537,870 -4.8%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 48,152,043 49,242,205 -2.2%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 46,003 46,970 -2.1%
56 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 3,879 3,879 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 11,851 11,851 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 43,398 43,398 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 19,365 19,365 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 68,814 68,814 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 76,781 76,781 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 2,214 2,214 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 5,939 5,939 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 177,421 180,028 -1.4%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 191,870 193,123 -0.6%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 888,298 893,033 -0.5%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 980,395 995,928 -1.6%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 2,744,346 2,783,255 -1.4%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 10,140,490 10,327,559 -1.8%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 12,720,265 12,647,877 +0.6%
binary_benchmark::marshal_group::bench_marshal_allocating 95,585 95,585 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 95,618 95,618 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 113,974 113,974 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 102,895 102,895 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 95,685 95,685 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 15,762 15,762 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 15,806 15,806 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 17,592 17,592 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 533,122 533,122 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 532,688 532,688 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 534,046 534,046 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 13,413,012 13,413,012 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 13,357,245 13,357,245 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 26,652,528 26,652,528 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 2,498 2,498 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 38,500 38,500 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 773 773 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 556,090 556,090 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 5,039 5,039 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 7,484 7,484 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 90,824 90,824 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 7,511 7,511 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 90,860 90,860 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 8,838 8,838 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 104,670 104,670 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 475,970 475,970 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 13,493 13,493 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 17,384,106 17,301,034 +0.5%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 160,994 162,069 -0.7%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 5,511,244 5,512,660 -0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 161,731 163,085 -0.8%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 298,353 298,493 -0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 712,883 713,231 -0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 12,691,261 12,659,018 +0.3%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 27,634,930 27,425,879 +0.8%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 3,467,011 3,467,011 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 125,419,583 124,270,023 +0.9%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,830,493 2,830,452 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 5,105,272 5,119,851 -0.3%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 297,627 299,173 -0.5%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 14,247,117 14,247,117 +0.0%

@jlucaso1
jlucaso1 force-pushed the perf/protocol-address-reuse branch 2 times, most recently from 4667310 to 6a3650c Compare April 12, 2026 17:28

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wacore/src/types/jid.rs (1)

151-183: 🧹 Nitpick | 🔵 Trivial

The new agent-path coverage is still self-referential.

The added cases include "100000000000001.1:75@lid", but the expected side calls to_protocol_address*(), which uses the same formatter under test. If agent handling regresses, these assertions still pass. Please add one literal expected-string assertion for an agent-bearing JID.

🧪 Suggested test hardening
     #[test]
+    fn test_agent_is_preserved_in_signal_and_protocol_addresses() {
+        let jid = Jid::from_str("100000000000001.1:75@lid").unwrap();
+        assert_eq!(jid.to_signal_address_string(), "100000000000001.1:75@lid");
+        assert_eq!(jid.to_protocol_address_string(), "100000000000001.1:75@lid.0");
+    }
+
+    #[test]
     fn test_reset_protocol_address_matches_fresh() {
         let jids = [
             "123456789@lid",
             "123456789:33@lid",
             "100000000000001.1:75@lid",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/types/jid.rs` around lines 151 - 183, The tests use
to_protocol_address() / to_protocol_address_string() (and
reset_protocol_address()/make_reusable_protocol_address()) to compute both
actual and expected values for agent-bearing JIDs, making the check
self-referential; add a literal expected-string assertion for at least one
agent-bearing JID (e.g. the "100000000000001.1:75@..." case) so the test
compares the formatter output to a hard-coded expected protocol-address string
rather than calling to_protocol_address*() for the expected value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wacore/libsignal/src/core/address.rs`:
- Around line 310-318: The display buffer is under-reserved: change
ProtocolAddress::with_capacity to allocate for the full ".{device_id}" suffix
(dot plus the decimal digits of device_id) instead of the fixed +2; compute the
decimal width of device_id (e.g., number of digits via to_string().len() or a
small digit-count routine) and use capacity + 1 + decimal_digits when
constructing display: String::with_capacity(capacity + 1 + decimal_digits), so
the first reset_name() remains zero-allocation.

---

Outside diff comments:
In `@wacore/src/types/jid.rs`:
- Around line 151-183: The tests use to_protocol_address() /
to_protocol_address_string() (and
reset_protocol_address()/make_reusable_protocol_address()) to compute both
actual and expected values for agent-bearing JIDs, making the check
self-referential; add a literal expected-string assertion for at least one
agent-bearing JID (e.g. the "100000000000001.1:75@..." case) so the test
compares the formatter output to a hard-coded expected protocol-address string
rather than calling to_protocol_address*() for the expected 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1f5a9c9c-c24a-43a0-9d9a-98dd1e18ee47

📥 Commits

Reviewing files that changed from the base of the PR and between ff58633 and 4667310.

📒 Files selected for processing (4)
  • src/send.rs
  • wacore/libsignal/src/core/address.rs
  • wacore/src/send.rs
  • wacore/src/types/jid.rs

Comment thread wacore/libsignal/src/core/address.rs Outdated
@jlucaso1
jlucaso1 force-pushed the perf/protocol-address-reuse branch from 6a3650c to ff514e5 Compare April 12, 2026 17:32

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wacore/src/types/jid.rs (1)

133-199: ⚠️ Potential issue | 🟠 Major

Use obviously synthetic JIDs in these tests.

Several fixtures here are formatted like live phone/JID identifiers (5511999887766...). Please replace them with unmistakably fake values before merging so we do not check plausible PII into the repo. As per coding guidelines, "Applies to **/test.rs : Never use real PII (phone numbers and JIDs) in test code; use fictitious values instead".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/types/jid.rs` around lines 133 - 199, Tests use plausible real
phone-style JIDs (e.g. "5511999887766..."); replace all such values with clearly
synthetic identifiers in the tests (e.g. "5550000000000@s.whatsapp.net" and
matching variants like "5550000000000:33@s.whatsapp.net" or other obviously fake
long ids) and update the expected strings in assertions accordingly (affecting
test_signal_address_string_phone,
test_protocol_address_string_matches_to_string,
test_reset_protocol_address_matches_fresh, and test_write_functions_dry as well
as any helper like make_reusable_protocol_address uses); keep the same
formatting/structure (e.g. .to_signal_address_string() -> "5550000000000@c.us",
protocol address strings -> "id@lid.0") so the logic is unchanged, only the
input/output literals are replaced with synthetic values.
♻️ Duplicate comments (1)
wacore/libsignal/src/core/address.rs (1)

317-345: ⚠️ Potential issue | 🟠 Major

with_capacity() leaves the cached key invalid until the first reset.

This constructor returns a ProtocolAddress with display == "", but Display, Eq, Ord, and Hash all read that field. Any accidental use before reset_with() will therefore hit the empty session key, and two fresh values with different device_ids compare equal.

♻️ Minimal fix
     pub fn with_capacity(capacity: usize, device_id: DeviceId) -> Self {
         // display = "{name}.{device_id}" — reserve for dot + decimal digits
         let suffix_len = 1 + digit_count(u32::from(device_id));
-        ProtocolAddress {
+        let mut address = ProtocolAddress {
             name: String::with_capacity(capacity),
             device_id,
             display: String::with_capacity(capacity + suffix_len),
-        }
+        };
+        address.rebuild_display();
+        address
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/libsignal/src/core/address.rs` around lines 317 - 345, with_capacity()
currently leaves display empty which makes Display/Eq/Ord/Hash observe an
incorrect cached key; fix by populating the cached display before returning:
after constructing the ProtocolAddress (with name String::with_capacity and
display String::with_capacity(...)) call rebuild_display() (or otherwise
format/push '.' + device_id into display) so the returned value has a valid
display string consistent with reset_with(), ensuring
ProtocolAddress::with_capacity, rebuild_display, reset_with and consumers of
Display/Eq/Ord/Hash behave correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@wacore/src/types/jid.rs`:
- Around line 133-199: Tests use plausible real phone-style JIDs (e.g.
"5511999887766..."); replace all such values with clearly synthetic identifiers
in the tests (e.g. "5550000000000@s.whatsapp.net" and matching variants like
"5550000000000:33@s.whatsapp.net" or other obviously fake long ids) and update
the expected strings in assertions accordingly (affecting
test_signal_address_string_phone,
test_protocol_address_string_matches_to_string,
test_reset_protocol_address_matches_fresh, and test_write_functions_dry as well
as any helper like make_reusable_protocol_address uses); keep the same
formatting/structure (e.g. .to_signal_address_string() -> "5550000000000@c.us",
protocol address strings -> "id@lid.0") so the logic is unchanged, only the
input/output literals are replaced with synthetic values.

---

Duplicate comments:
In `@wacore/libsignal/src/core/address.rs`:
- Around line 317-345: with_capacity() currently leaves display empty which
makes Display/Eq/Ord/Hash observe an incorrect cached key; fix by populating the
cached display before returning: after constructing the ProtocolAddress (with
name String::with_capacity and display String::with_capacity(...)) call
rebuild_display() (or otherwise format/push '.' + device_id into display) so the
returned value has a valid display string consistent with reset_with(), ensuring
ProtocolAddress::with_capacity, rebuild_display, reset_with and consumers of
Display/Eq/Ord/Hash behave correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: db55d47b-a4e1-4406-bdb8-2a301ae948e2

📥 Commits

Reviewing files that changed from the base of the PR and between 6a3650c and ff514e5.

📒 Files selected for processing (4)
  • src/send.rs
  • wacore/libsignal/src/core/address.rs
  • wacore/src/send.rs
  • wacore/src/types/jid.rs

@jlucaso1
jlucaso1 force-pushed the perf/protocol-address-reuse branch from ff514e5 to cabed27 Compare April 12, 2026 17:46
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Apr 12, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 3m 51s —— View job


Code Review: perf: reusable ProtocolAddress in encrypt_for_devices hot loops

  • Read diff and all changed files
  • Review wacore/libsignal/src/core/address.rs
  • Review wacore/src/types/jid.rs
  • Review wacore/src/send.rs
  • Review src/send.rs
  • Check resolution of previous review items
  • Post review feedback

The single-buffer ProtocolAddress redesign is clean and correct. The reset_with() closure pattern is elegant — one write pass, no intermediate copy. All three previous review items have been addressed. This looks good to merge with just minor nits below.

Previous review items — all resolved

# Issue Status
1 display not pre-allocated in make_reusable_protocol_address() Fixed — with_capacity now sizes buffer with digit_count()
2 Wasted ProtocolAddress allocation in session_mutexes_for Fixed — make_address_buffer() added, used at src/send.rs:1518
3 Dotted LID test coverage removed Fixed — "100000000000001.1:75@lid" in both test_protocol_address_string_matches_to_string and test_reset_protocol_address_matches_fresh with hard-coded expected strings

Nits

1. Dead initial write in with_capacitywacore/libsignal/src/core/address.rs:328

pub fn with_capacity(capacity: usize, device_id: DeviceId) -> Self {
    use std::fmt::Write;
    let suffix_len = 1 + digit_count(u32::from(device_id));
    let mut buf = String::with_capacity(capacity + suffix_len);
    write!(buf, ".{device_id}").unwrap();  // ← cleared on first reset_with()
    Self { buf, name_len: 0, device_id }
}

reset_with() calls self.buf.clear() before writing, so the initial .0 is immediately discarded on first use. This leaves the address in an odd intermediate state where name() returns "" and as_str() returns ".0". Since the doc says "Call reset_with() to fill" and all callers follow that contract, this isn't a bug — but the write is dead work. You could just leave buf empty:

let buf = String::with_capacity(capacity + suffix_len);
Self { buf, name_len: 0, device_id }

2. Double mapped_server call in to_signal_address_string / to_protocol_address_stringwacore/src/types/jid.rs:94-110

Both methods call mapped_server() once for the capacity hint, then write_signal_address_to() calls it again internally. It's just a string comparison so the cost is negligible, but if you wanted to tighten it, you could pass the pre-computed server into the write function or just use SIGNAL_ADDRESS_CAPACITY for the hint (like the reusable path does).

3. test_write_functions_dry is self-referentialwacore/src/types/jid.rs:194-203

write_signal_address_to(&jid, &mut buf);
assert_eq!(buf, jid.to_signal_address_string());  // delegates to same function

This only validates that the public wrapper calls the helper correctly (round-trip consistency), not that the output is correct. Consider adding one hard-coded expected value:

write_signal_address_to(&jid, &mut buf);
assert_eq!(buf, "15550000001@c.us");

Correctness notes

  • The single-buffer design is sound. name()&buf[..name_len], as_str()&buf — no ambiguity since device_id is always a decimal integer (no dots), so "{name}.{device_id}" is always unambiguously parseable.
  • Eq/Hash/Ord all operate on the full buf string, which is correct — same (name, device_id) always produces the same buf.
  • The 40+ cold-path to_protocol_address() call sites across src/message.rs, src/features/signal.rs, src/client/sessions.rs, etc. all benefit from the reduced struct size (2 Strings → 1) automatically.
  • The encrypt_for_devices hot loop correctly reuses one ProtocolAddress across all four per-device phases (LID check, direct check, prekey processing, encryption).

LGTM — nits are all optional.


DRY the address formatting: extract write_signal_address_to() as the
core helper, make write_protocol_address_to() and all JidExt methods
delegate to it. Replace magic strings with existing constants
(DEFAULT_USER_SERVER, LEGACY_USER_SERVER).

Add ProtocolAddress::reset_name() for in-place string rewrite.
Add DeviceId::new() const constructor.
Add make_reusable_protocol_address() factory for hot-loop use.
Add SIGNAL_ADDRESS_CAPACITY (64) and SIGNAL_DEVICE_ID constants.

In encrypt_for_devices(), reuse one buffer + ProtocolAddress across
all 4 hot loops (LID session check, direct session check, prekey
processing, message encryption). For a 100-device group, this
eliminates ~200 String allocations per message send.

Buffer capacity of 64 bytes based on real WhatsApp logs showing
max signal address length of 53 chars.
@jlucaso1
jlucaso1 force-pushed the perf/protocol-address-reuse branch from cabed27 to ed0b21a Compare April 12, 2026 17:58
@jlucaso1 jlucaso1 changed the title perf: reusable ProtocolAddress in encrypt_for_devices hot loops perf: single-buffer ProtocolAddress + reusable hot-loop address construction Apr 12, 2026
@jlucaso1
jlucaso1 merged commit 1007676 into main Apr 12, 2026
8 checks passed
@jlucaso1
jlucaso1 deleted the perf/protocol-address-reuse branch April 12, 2026 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant