Skip to content

perf: four more cuts to the per-message allocation count - #1137

Merged
jlucaso1 merged 12 commits into
mainfrom
perf/alloc-round4
Jul 27, 2026
Merged

perf: four more cuts to the per-message allocation count#1137
jlucaso1 merged 12 commits into
mainfrom
perf/alloc-round4

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Four independent cuts to the DM round trip, one commit each, from two independent analyses of the same dhat profile (one Opus, one Codex, neither aware of the other). As in the previous round they overlapped almost not at all: two items come from each.

The frame decoder change those analyses also surfaced went out as #1136, kept separate because its risk is retention rather than correctness and it deserved its own measurement. It has since merged, and this branch is rebased on top of it.

Denominator throughout is 116.74 blocks/msg, the library's share; the raw 123.76 includes 7.02 owned by the harness client.

The cuts

1. The chain key buffer is reused across ratchet advances (3.00/msg). Bytes is immutable, so existing.key = Some(Bytes::copy_from_slice(..)) allocated on every advance, and the ratchet advances three times per round trip: twice sending, once receiving. After a checkout the record is uniquely owned, since the cache takes it out of its Arc with try_unwrap, so the buffer already there is almost always ours to overwrite.

Two guards decide when it is not, and both matter: a buffer someone else still holds would show that holder a key it never asked for, and a buffer of the wrong length (a record from an older format) would end up half old and half new. Either case falls back to allocating, which is exactly the previous behaviour.

2. The burst callers own their results buffer (1.43/msg). send_raw_bytes_burst returned a Vec, so it allocated once per burst, and the common burst is a single frame: that allocation was the dominant cost of sending one. Both callers are long-lived worker loops that already reuse their frame and guard buffers. The function clears the buffer itself rather than trusting callers, since a stale result from the previous burst would be reported against the wrong frame.

3. The reporting token hashes its pieces instead of concatenating them (1.00/msg). The extractor already stages the whitelisted fields as ranges and sorts them; the Vec existed only to hand the HMAC one slice. Mac::update is associative over its input, so feeding each piece in token order hashes exactly the bytes the concatenation would have held. extract_reporting_token_content stays for callers that want the bytes.

4. The JID names itself into the DSM field (1.00/msg). That field writes the destination's length before its bytes, so the send path rendered the JID into a String purely to measure it and copy it out again. DsmDestination lets a Jid do both without the intermediate, and both share one body rather than the DSM format existing in two shapes.

Replacing a &str parameter with a generic one costs deref coercion, which took four rounds of review to get right and is written up under the findings below.

Measurements

Harness pingpong, 120k messages at 12k/s, MODE=rss, three interleaved ABBA/BAAB pairs against main (d2a3a16a), pre-built binaries with the sha256 recorded per run. Every run had lost=0 and ack=120000.

metric main branch delta Welch t
allocator calls per message 127.76 (sd 0.12) 121.23 (sd 0.42) -5.11% -25.18
bytes requested per message 27 392 (sd 3) 27 220 (sd 12) -0.63% -24.08

6.53 allocations per message removed, against 6.43 predicted by summing the four items from the profile. One baseline run is excluded: its wchar came in at 114 MB against ~81 MB for every other run, which is host I/O in that window rather than branch behaviour. Including it gives -5.76%, the same story with a worse denominator.

No CPU claim. These are small allocations served by a warm allocator; earlier profiling on this workload put malloc+free at ~2.8% with the bottleneck in curve25519. Worth doing for allocator pressure, not for a throughput promise.

What each change risks, and what pins it

The two items that touch wire or crypto state carry the real risk, so they carry the real tests.

The chain key is key material. Three mutations, all killed: removing the shared-buffer check (the uniquely-owned test fails, since the buffer is silently replaced instead of reused), removing the length check (a 16-byte buffer gets overwritten with 32 bytes of key), and the absent-buffer path.

The DSM destination writes a length prefix ahead of its payload. A counted length that disagrees with the bytes written would point past the payload, and the peer would read the next field from the wrong offset. A test pins the count against the render across every server form, agent and device combination, and multibyte user parts; mutating the counter by one byte fails it.

The reporting token goes on the wire and the server verifies it. The identity test covers a flat field, a nested one, multibyte content, and a two-field message. That last case is the one that matters and it was added after a mutation survived: with a single piece the visit order cannot be observed, so reversing it passed unnoticed. With two fields, reversing the order now fails the test.

The burst buffer is cleared by the callee; removing that clear leaks the previous burst's results into the next one, which the test catches as 5 results where 4 were sent.

Deliberately left out

Caching the own-identity Arc<str> (would be 1.00/msg). The identity comes from the persistence manager's device snapshot and changes on a re-pair. A parallel cache that falls out of sync writes the messageSecret under the wrong identity, and reading those secrets back (msmsg replies) then fails. The safe shape is to hold the Arc<str> inside the snapshot itself, which is a much larger change than the item suggests. Not worth it for one allocation.

A peer-JID memo for the same site was speculative even in the analysis that proposed it: it puts a lock on the hot path to save an allocation, which may cost more wall clock than it saves.

Reusing the session record's Arc across checkout and commit (3.00/msg). It means changing SessionCheckout::commit to take an Arc<SessionRecord>, a cross-crate trait signature on the encryption path, where a mistake is a ratchet desync rather than a crash. Not while cheaper items remain.

Verification

cargo fmt --all, cargo clippy --workspace --all-targets with zero warnings, RUSTDOCFLAGS="-D warnings" cargo doc clean, and green suites: wacore 1268, whatsapp-rust 1230, wacore-libsignal 199.

Review findings folded in after opening

Two tests of mine proved nothing, and only mutation said so. The burst test captured the results pointer between the two calls, so a single-frame path that replaced the caller's buffer would have been compared against its own replacement; it is captured before the first call now. And calculate_reporting_token_over_pieces hashed whatever it was handed, so an empty list would have minted a valid token over no content -- unreachable today, since both callers pass through the collector, but the test named for that property only exercised the collector. It asks the HMAC directly now.

A regression this PR introduced: the streaming path collected the pieces once to test for content and again to hash them, so a nested message allocated four times where it used to allocate three. Collected once now, pinned by a call tally that reads 2 for a nested message and 4 if the second collection returns.

Replacing &str with a generic parameter broke deref coercion, in four widening steps. &String first, then &Box<str> / &Rc<str> / &Arc<str> / &Cow<str>, then the mutable forms plus a Copy bound that no &mut satisfies, and finally nested references such as &&str -- which no finite list of implementations can cover, since there is always one more level than it names.

The shape it landed on: implement the trait on the owned types, and let two blanket implementations carry it through &T and &mut T. That covers any depth and either mutability at once.

Recorded because it was proposed twice and does not work: a blanket over Deref<Target = str> collides three ways simultaneously (E0119 against &_, &mut _, and Jid) -- Jid because coherence cannot rule out it gaining that Deref, and the reference cases because &str derefs to str too.

Declined: keeping a &str entry point alongside the generic one so that foreign string newtypes keep coercing. It restores the two callable shapes this item set out to remove, with the &str entry point existing purely to preserve a coercion, while the only in-tree caller passes a &Jid. A newtype has &*wrapper, or can implement the public trait; both are documented on it now.

CodSpeed flags two regressions; neither warrants a change. bench_reject_prekey_as_signal ran on a different CI runner (Simulation mode derives its cache model from the physical CPU) and its measured body is SignalMessage::try_from plus an error assertion, which touches nothing here. bench_content_extraction_simple is ~1.2us on a benchmark that is ~72% message_to_vec, code this PR does not touch. The allocation win the change was made for shows up where it should: bench_full_token_generation_simple goes 30 B -> 15 B.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR introduces caller-owned burst result buffers, reusable session chain-key storage, direct JID destination encoding for DSM messages, and streaming reporting-token HMAC calculation. Call sites and tests are updated for the revised APIs and behavior.

Changes

Burst result buffering

Layer / File(s) Summary
Caller-owned burst results
src/client/messaging.rs
send_raw_bytes_burst now clears and fills a caller-provided results buffer while preserving frame draining and error handling.
Worker integration and validation
src/client/node_io.rs, src/message/dispatch.rs, src/client/tests.rs
ACK and receipt workers drain supplied results, while tests cover reuse, draining, and per-frame errors.

Chain-key buffer reuse

Layer / File(s) Summary
In-place chain-key updates
wacore/libsignal/src/protocol/state/session.rs
Sender and receiver chain-key setters conditionally reuse uniquely owned buffers, with tests for ownership, length, and absent-field cases.

DSM destination encoding

Layer / File(s) Summary
Destination encoding contract
wacore/src/messages.rs
Adds DsmDestination with encoded-length and direct-write operations for strings and JIDs.
Direct DSM framing
wacore/src/messages.rs, wacore/src/send/dm.rs
DM encoders and send preparation write JIDs directly into framed output; tests compare JID and string encodings.

Streaming reporting tokens

Layer / File(s) Summary
Ordered reporting pieces
wacore/src/reporting_token.rs
Whitelist extraction now collects ordered borrowed or rebuilt protobuf pieces while retaining concatenated extraction.
Streaming token generation
wacore/src/reporting_token.rs
Reporting tokens are calculated through incremental HMAC updates, with tests confirming equivalence and empty-content behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: performance, api-design

Suggested reviewers: greptile-apps, cubic-dev-ai

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main goal: reducing per-message allocations across the changeset.
Description check ✅ Passed The description is directly related to the PR and accurately describes the allocation-reduction changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/alloc-round4

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.

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown

Greptile Summary

This PR reduces per-message allocations while preserving existing send-path behavior.

  • Reuses chain-key storage when the protobuf buffer is uniquely owned and correctly sized.
  • Moves burst-send results into caller-owned reusable buffers.
  • Streams reporting-token pieces directly into the HMAC.
  • Encodes DSM destinations without first allocating a rendered JID string.
  • Adds regression tests for buffer ownership, token equivalence, destination encoding, and generic reference compatibility.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previous generic destination compatibility issues are resolved by recursive shared and mutable reference implementations with compile-time coverage for the reported caller shapes.

Important Files Changed

Filename Overview
wacore/src/messages.rs Introduces allocation-free DSM destination encoding and blanket reference implementations that resolve the previously reported generic coercion regressions.
wacore/src/reporting_token.rs Streams ordered reporting-token pieces into the HMAC while retaining concatenated extraction for existing callers.
wacore/libsignal/src/protocol/state/session.rs Reuses uniquely owned, same-length chain-key buffers while preserving allocating fallbacks.
src/client/messaging.rs Changes burst sending to populate and clear a caller-owned reusable results buffer.
wacore/src/send/dm.rs Passes JIDs directly into DSM encoding instead of allocating intermediate strings.

Sequence Diagram

sequenceDiagram
  participant Send as DM send path
  participant DSM as MessageUtils
  participant Token as Reporting token
  participant Signal as Signal session
  participant Socket as Burst sender
  Send->>DSM: "Encode plaintext with &Jid"
  DSM-->>Send: Recipient and own-device bytes
  Send->>Token: Collect whitelisted pieces once
  Token-->>Send: Streamed HMAC token
  Send->>Signal: Encrypt per device
  Signal->>Signal: Reuse eligible chain-key buffer
  Send->>Socket: Send frames into reusable results buffer
  Socket-->>Send: Per-frame results
Loading

Reviews (9): Last reviewed commit: "docs(client): mark what the burst out-pa..." | Re-trigger Greptile

Comment thread wacore/src/messages.rs

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

Caution

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

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

828-847: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Reuse the collected pieces instead of parsing and rebuilding them twice.

Line 830 builds pieces, then Line 847 indirectly calls collect_reporting_token_pieces again. Nested fields therefore rebuild their owned framing twice, which can negate—or exceed—the allocation savings this PR targets. Pass the first collection into the HMAC helper.

Proposed fix
-    collect_reporting_token_pieces(encoded_message, REPORTING_FIELDS)?;
+    let pieces = collect_reporting_token_pieces(encoded_message, REPORTING_FIELDS)?;
 ...
-    let token = calculate_reporting_token_streaming(&key, encoded_message, REPORTING_FIELDS)?;
+    let token = calculate_reporting_token_from_pieces(&key, encoded_message, &pieces)?;
fn calculate_reporting_token_from_pieces(
    reporting_token_key: &[u8; REPORTING_TOKEN_KEY_SIZE],
    data: &[u8],
    pieces: &[(u32, Piece)],
) -> Option<[u8; REPORTING_TOKEN_SIZE]> {
    let mut mac = Hmac::<Sha256>::new_from_slice(reporting_token_key).ok()?;

    for (_, piece) in pieces {
        match piece {
            Piece::Borrowed(range) => mac.update(&data[range.clone()]),
            Piece::Owned(bytes) => mac.update(bytes),
        }
    }

    let result = mac.finalize().into_bytes();
    let mut token = [0u8; REPORTING_TOKEN_SIZE];
    token.copy_from_slice(&result[..REPORTING_TOKEN_SIZE]);
    Some(token)
}

This defeats the stated allocation-reduction objective.

🤖 Prompt for 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.

In `@wacore/src/reporting_token.rs` around lines 828 - 847, Reuse the pieces
collected by collect_reporting_token_pieces instead of invoking that function
again through calculate_reporting_token_streaming. Update the reporting-token
flow and HMAC helper to accept the collected [(u32, Piece)] data, iterate over
borrowed and owned pieces, and preserve the existing token output.
🤖 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 `@src/client/tests.rs`:
- Around line 3430-3435: Strengthen the results-buffer reuse assertion around
the burst calls by preallocating the caller-owned Vec to the maximum burst size
and recording its as_ptr() before each call. Assert that the pointer remains
identical across calls, rather than only checking results.capacity(), while
preserving the existing out-parameter reuse behavior.

In `@wacore/src/messages.rs`:
- Around line 19-21: Document in the DsmDestination trait that encoded_len()
must exactly match the number of bytes write_into() appends to out, or seal the
trait if custom implementations are not supported and only &str and &Jid are
intended.

In `@wacore/src/send/dm.rs`:
- Around line 181-182: Update the preceding comment in the send flow to state
only that the unused DSM buffer is skipped, removing any claim about avoiding
destination-JID stringification. Delete the duplicate rationale comment adjacent
to the wire-buffer handling, keeping the allocation rationale solely at the
decision point.

---

Outside diff comments:
In `@wacore/src/reporting_token.rs`:
- Around line 828-847: Reuse the pieces collected by
collect_reporting_token_pieces instead of invoking that function again through
calculate_reporting_token_streaming. Update the reporting-token flow and HMAC
helper to accept the collected [(u32, Piece)] data, iterate over borrowed and
owned pieces, and preserve the existing token output.
🪄 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 (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a4d6277a-71ed-4c65-90d9-17e380c1e7bf

📥 Commits

Reviewing files that changed from the base of the PR and between d2a3a16 and 70a8b69.

📒 Files selected for processing (8)
  • src/client/messaging.rs
  • src/client/node_io.rs
  • src/client/tests.rs
  • src/message/dispatch.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/src/messages.rs
  • wacore/src/reporting_token.rs
  • wacore/src/send/dm.rs

Comment thread src/client/tests.rs Outdated
Comment thread wacore/src/messages.rs
Comment thread wacore/src/send/dm.rs Outdated
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.01 MiB 10.01 MiB +3.00 KiB (+0.03%) 🔺
bin .text 8.06 MiB 8.06 MiB +2.69 KiB (+0.03%) 🔺
bin allocated (text+data+bss) 10.01 MiB 10.01 MiB +4.05 KiB (+0.04%) 🔺
llvm-lines wacore 493,295 494,045 +750 (+0.15%) 🔺
llvm-lines wacore copies 16,368 16,380 +12 (+0.07%) 🔺
llvm-lines whatsapp-rust lib 720,432 720,874 +442 (+0.06%) 🔺
llvm-lines whatsapp-rust lib copies 22,771 22,784 +13 (+0.06%) 🔺
deps crates (Cargo.lock) 471 471 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.83 MiB 1.83 MiB +575 B (+0.03%) 🔺
.text wacore 655.05 KiB 656.64 KiB +1.59 KiB (+0.24%) 🔺
.text wacore_binary 89.69 KiB 89.69 KiB 0
.text wacore_libsignal 171.09 KiB 171.38 KiB +303 B (+0.17%) 🔺
.text wacore_appstate 22.34 KiB 22.34 KiB 0
.text wacore_noise 21.79 KiB 21.79 KiB 0
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 515.17 KiB 515.17 KiB 0
.text whatsapp_rust_tokio_transport 39.91 KiB 39.91 KiB 0
.text whatsapp_rust_ureq_http_client 10.33 KiB 10.33 KiB 0
.text std 1.07 MiB 1.07 MiB +179 B (+0.02%) 🔺
.text other deps 1.89 MiB 1.89 MiB 0
Top movers (cargo-bloat attribution)
Crate main PR Δ
wacore 655.05 KiB 656.64 KiB +1.59 KiB (+0.24%)

Baseline: 1d38b27d2 (latest main run) · Head: 311050031 · Graphs

@codspeed-hq

codspeed-hq Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will regress 0 benchmarks

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 2 (👁 2) regressed benchmarks
✅ 195 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory bench_full_token_generation_simple 30 B 15 B ×2
👁 Simulation bench_content_extraction_simple 10.2 µs 11.4 µs -10.25%
👁 Simulation bench_reject_prekey_as_signal 1.1 µs 1.3 µs -13.43%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/alloc-round4 (56a02c3) with main (1d38b27)

Open in CodSpeed

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70a8b690a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/reporting_token.rs Outdated
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@codspeedbot why the performance regrets

jlucaso1 added 6 commits July 27, 2026 06:39
Bytes is immutable, so assigning a fresh copy_from_slice allocated on every
advance, and the ratchet advances three times per message round trip: twice
sending, once receiving.

After a checkout the record is uniquely owned, since the cache takes it out
of its Arc with try_unwrap, so the buffer already there is almost always
ours to overwrite. A buffer that is still shared, or one of an unexpected
length from an older record, falls back to allocating exactly as before:
overwriting a shared buffer would show a holder a key it never asked for,
and overwriting a shorter one would leave a key half old and half new.
send_raw_bytes_burst returned a Vec, so it allocated once per burst, and
the common burst is a single frame: that allocation was the dominant cost
of sending one. Both callers are long-lived worker loops that already reuse
their frame and guard buffers, so they reuse this one too.

The function clears the buffer up front rather than trusting callers, since
a stale result from the previous burst would be reported against the wrong
frame.
…first

The extractor already stages the whitelisted fields as ranges and sorts
them; the Vec existed only to hand the HMAC one slice. Mac::update is
associative over its input, so feeding each piece in token order hashes
exactly the bytes the concatenation would have held, without building it.

extract_reporting_token_content stays for callers that want the bytes.

The token goes on the wire and the server verifies it, so a divergence
here is a rejected message rather than a slow one. The identity test covers
a flat field, a nested one, multibyte content, and a two-field message:
that last case is the one that matters, since with a single piece the order
cannot be observed and reversing the visit order passes unnoticed.
The DSM field writes the destination's length before its bytes, so the
send path rendered the JID into a String purely to measure it and copy it
out again.

DsmDestination lets a Jid do both without the intermediate: count what a
render would write, then write it straight into the wire buffer. The &str
impl keeps every existing caller working, and both share one body rather
than the DSM format existing in two shapes.

The hazard is a counted length that disagrees with the bytes written: the
prefix would then point past the payload and the peer would read the next
field from the wrong offset. A test pins the two against each other across
every server form, agent and device combination, and multibyte user parts.
The streaming reporting token collected its pieces twice: once to test for
content and once to hash them. A nested field owns its re-framed bytes, so
for any message carrying media or quoted text that materialised those
buffers twice and parsed the protobuf twice, making the change slower than
the concatenation it replaced. Collected once and reused now, with a
test-only tally pinning it: reintroducing the second collection takes the
count from 2 to 4.

DsmDestination gains a &String impl, because a generic parameter does not
deref-coerce the way the old &str argument did, and its contract is now
documented on the trait: encoded_len must equal what write_into appends, or
the length prefix points past its own payload.

The burst buffer test compared capacity, which a fresh Vec of the same
capacity satisfies. It compares the allocation itself now, and fails when
the callee replaces the buffer with an equal one.

Also drops a stale rationale in dm.rs that still described the JID
stringification this PR removed.
@jlucaso1
jlucaso1 force-pushed the perf/alloc-round4 branch from ceccdef to 8db919f Compare July 27, 2026 09:41
@codspeed-hq

codspeed-hq Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@codspeedbot why the performance regrets

Looked into both flagged benchmarks. Neither is a real regression on the DM path this PR targets — one is runner noise, the other is a small, benign shift on a benchmark dominated by code this PR doesn't touch.

bench_reject_prekey_as_signal (Simulation, −13.43%) — runner difference, not this PR

This one is listed in CodSpeed's "Different runtime environments detected" section: the base ran on an INTEL(R) XEON(R) PLATINUM 8573C and the head on an Intel(R) Xeon(R) 6973P-C. In Simulation mode the cache model is derived from the physical CPU, so a different runner shifts the cycle estimate on identical source (docs).

The benchmark itself only does SignalMessage::try_from(ciphertext) and asserts it errors — it never advances a ratchet, touches chain keys, or exercises anything the four cuts changed. So there's nothing in the diff that could move it; the delta is the CPU swap.

bench_content_extraction_simple (Simulation, −10.02%) — real but tiny, and mostly outside the changed code

This one was compared on matching runners, so the change is real — but it's ~1.2µs (10.2µs → 11.4µs) on a benchmark that is not on the DM hot path this PR optimizes.

From the head flamegraph, ~72% of the benchmark is waproto::codec::message_to_vec (protobuf compute_size + write_to), which this PR does not touch. The extraction call this PR did change, extract_reporting_token_content, is only ~22% of the total. The measured increase is almost entirely memory-access time (the is_some/is_set protobuf field probes are 90%+ memory in the breakdown), i.e. cache-layout sensitivity in the protobuf encode step rather than added work in the extractor.

Where the extractor did change: it now routes through a new collect_reporting_token_pieces that returns a SmallVec<[(u32, Piece); 4]>, and extract_reporting_token_content then walks that to build the Vec. That adds a small intermediate structure and a call boundary versus the old single-pass function, which is a plausible few-percent on this micro-benchmark. Note this benchmark exercises the extract_...content (concatenating) path, not the streaming generate_reporting_token_from_encoded path the PR actually optimizes for the send hot path — and the memory-mode result confirms the intended win: bench_full_token_generation_simple went 30 B → 15 B (×2 fewer allocations).

Bottom line

  • The prekey regression is a CI runner CPU change, not a code effect — safe to disregard.
  • The content-extraction regression is a small, real shift concentrated in unchanged protobuf-encode code (cache layout), on a path that isn't the target of this PR; the allocation win the PR set out to get shows up as expected on the memory benchmark.

Neither warrants a code change. If you'd like the extractor micro-cost gone too, the cleanest option is to have extract_reporting_token_content build the Vec directly again and only use collect_reporting_token_pieces on the streaming path, so the concatenating callers don't pay for the extra SmallVec hop — happy to do that if you want it.

Summary
  • Warming up the performance wizard...
  • Handling comment — Comment handling finished successfully

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

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

🤖 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 `@src/client/tests.rs`:
- Around line 3412-3440: Capture the preallocated results buffer pointer
immediately after Vec::with_capacity(4), before the first send_raw_bytes_burst
call. Assert results.as_ptr() matches that pointer after the single-frame burst
and retain the existing identity assertion after the four-frame burst.

In `@wacore/src/reporting_token.rs`:
- Around line 740-757: Update calculate_reporting_token_over_pieces to return
None immediately when pieces is empty, before initializing or finalizing the
HMAC, while preserving the existing token calculation for non-empty pieces.
Extend a_message_with_nothing_to_report_produces_no_token to directly invoke the
streaming calculation path with the test key and assert it returns None, rather
than only exercising piece collection.
- Around line 815-818: Remove the call-site comment above
collect_reporting_token_pieces in the reporting-token flow, keeping the
rationale only in calculate_reporting_token_over_pieces’ documentation. Leave
the pieces collection and subsequent HMAC behavior unchanged.
🪄 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 (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b3f599ce-2268-47c0-b5be-dd4ed9f297af

📥 Commits

Reviewing files that changed from the base of the PR and between 70a8b69 and 8db919f.

📒 Files selected for processing (8)
  • src/client/messaging.rs
  • src/client/node_io.rs
  • src/client/tests.rs
  • src/message/dispatch.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/src/messages.rs
  • wacore/src/reporting_token.rs
  • wacore/src/send/dm.rs

Comment thread src/client/tests.rs
Comment thread wacore/src/reporting_token.rs
Comment thread wacore/src/reporting_token.rs Outdated
The burst test captured the results pointer between the two calls, so a
single-frame path that replaced the caller's buffer would have been
compared against its own replacement. Captured before the first call now,
and asserted after both; mutating that path to swap the buffer fails it.

`calculate_reporting_token_over_pieces` hashed whatever it was given, so
an empty list would have minted a valid token over no content. Both
callers reach it through the collector, which already returns `None` in
that case, but a token that says nothing must not depend on a caller
remembering to check. The test named for that property only exercised the
collector; it now asks the HMAC directly.

Also drops a rationale repeated at the call site of the function whose
doc comment already carries it.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2db2d2a75

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/messages.rs Outdated
A generic parameter does not deref-coerce, so replacing the `&str`
argument silently dropped every caller holding its destination in a
wrapper. `&String` was repaired when it was reported; `&Box<str>`,
`&Rc<str>`, `&Arc<str>` and `&Cow<'_, str>` had the same problem.

A blanket `impl<T: Deref<Target = str>>` would be one line but collides
with the `&Jid` implementation: coherence cannot rule out `Jid` gaining
that `Deref`, so the compiler treats the two as overlapping. Hence a
macro over a named list.

The test is a compile-time check as much as a runtime one, which is the
point: dropping a wrapper from the list fails the build here rather than
in a downstream crate.

@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 33-63: Update the destination trait implementations generated by
dsm_destination_via_str to cover mutable references (&mut $ty) for every
documented string wrapper, using the same encoded_len and write_into behavior as
immutable references. In encode_dm_plaintexts, remove the unnecessary Copy bound
so mutable destination call sites remain supported.
🪄 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 (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1ef3b595-4882-4adf-a263-db76431424a9

📥 Commits

Reviewing files that changed from the base of the PR and between b2db2d2 and ef07492.

📒 Files selected for processing (1)
  • wacore/src/messages.rs

Comment thread wacore/src/messages.rs Outdated
`&mut String` coerced to `&str` exactly as `&String` did, so the generic
parameter dropped it along with the rest. The macro now emits the mutable
form of each wrapper, and `str` itself moved into the list so `&mut str`
comes with it.

`encode_dm_plaintexts` additionally required `Copy`, which no `&mut`
reference satisfies. Nothing needed it: the destination is read through
`&self` and the one by-value use is a tail call.

The wrapper test goes through `encode_dm_plaintexts` as well as the
encoded-content entry point, since only the former carried that bound and
a test that never called it left the bound free to come back.
Comment thread wacore/src/messages.rs
A nested reference such as `&&str` coerced to the old `&str` argument and
did not reach any implementation in the list, and no finite list can:
there is a depth for every entry it names.

The trait is implemented on the owned types instead, with two blanket
implementations carrying it through `&T` and `&mut T`. That covers any
depth and either mutability, and removes the per-wrapper mutable copies
the previous commit added.

The earlier note stays true and stays recorded: a blanket over
`Deref<Target = str>` on the reference types is what does not work, since
coherence cannot rule out `Jid` gaining that `Deref`.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2bdf2a58b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/messages.rs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8d6fc9bb8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/client/messaging.rs
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