Skip to content

perf(messages): draw the pad length from the thread RNG - #1189

Merged
jlucaso1 merged 1 commit into
mainfrom
claude/random-pad-len-csprng-utgo78
Jul 29, 2026
Merged

jlucaso1 merged 1 commit into
mainfrom
claude/random-pad-len-csprng-utgo78

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

MessageUtils::random_pad_len built a whole CSPRNG to produce a single byte. rand::make_rng::<StdRng>() pulls a 32 byte seed from the thread generator, runs a full ChaCha12 key schedule and generates a 256 byte block, and all of that existed so the next line could return (x & 0x0F) + 1.

It now reads the thread-local generator directly. Same ChaCha12 CSPRNG, already seeded, so the entropy source is not weakened, and the distribution is untouched: still uniform over 1..=16.

This exact fix already exists in the tree. request.rs:219-222 does it for message_id_at, with the comment explaining why. It was applied there and never here.

Changes

  • wacore/src/messages.rs: random_pad_len uses rand::rng() instead of seeding a fresh StdRng per plaintext. The comment justifying the (x & 0x0F) + 1 distribution and the test that covers it are unchanged.

Cost

Per call, isolated harness, 3 rounds of 2000 samples of 1000 calls each, core pinned with taskset:

median ns/call retired instructions/call
make_rng::<StdRng>() 99.1 (99.1 / 99.2 / 99.1 across rounds) 759.5
rand::rng() 3.3 (3.38 / 3.25 / 3.26) 40.7
delta -95.8 ns -718.8

Instruction counts are callgrind, taken as the slope between a 1k call and a 101k call run so process startup cancels out. Deterministic, and therefore the number worth trusting.

random_pad_len runs twice per DM send: dm_plaintexts_from_encoded pads the recipient plaintext and the own-devices plaintext separately, so pad_message_v2 is called once for each. That predicts about 1438 instructions off a DM send, and callgrind over the before/after bench binaries agrees: the whole-process slope for bench_dm_send differs by 1439 instructions per iteration.

End to end, from CodSpeed's deterministic instrument (3fa980d against baseline 0247452):

Benchmark main PR Δ
bench_dm_recv_steady (control, untouched) 67.159 µs 67.166 µs +0.01%
bench_encode_and_pad (one pad draw) 16.42 µs 14.99 µs -8.7%
bench_dm_send_encode_work[text_reply] (two draws) 57.82 µs 57.13 µs -1.2%
bench_dm_send 144.19 µs 143.85 µs -0.24%

The saving is per pad draw and additive, and the control holding at +0.01% is what makes the rest readable. On main's flamegraph bench_encode_and_pad carries rand::make_rng::<StdRng> at 811 ns plus chacha20::ChaChaCore<R12>::generate at 627 ns, 8.8% of that benchmark; on the PR neither frame exists and random_pad_len is a single 216 ns leaf. bench_encode_and_pad reads 16.16 / 16.16 / 16.42 / 16.42 µs over the last four main runs, so 14.99 µs is outside baseline drift.

Nothing is badged as an improvement because -8.7% on one micro-bench did not clear this repo's CodSpeed threshold, and the run-level aggregate averages it across ~210 benchmark/instrument pairs.

Locally the wall-clock benches could not resolve this and I am not claiming anything from them: 3 interleaved before/after rounds at --min-time 3 on a pinned core put the control's own spread at about 11% between rounds, well above the roughly 190 ns being looked for. That is a property of the box, not of the change; CodSpeed's instrument is what settles it.

Magnitude, plainly: about 0.2% of a DM send. At any realistic message rate it is invisible. It is worth taking because it is a one line change that deletes pure work at zero risk, not because 190 ns matters.

Follow-ups (not touched here)

Other non-test make_rng::<StdRng>() sites exist on or near send paths. Each has its own call frequency and needs its own measurement, so they are deliberately left alone:

  • wacore/src/send/encrypt.rs:672
  • wacore/src/send/group.rs:522, :692
  • wacore/src/crypto.rs:80, :88
  • wacore/src/store/signal_cache.rs:20

Validation

  • cargo fmt --all
  • cargo test -p wacore --lib: 1319 passed, 0 failed. random_pad_len_is_uniform_1_to_16 passes with no adaptation, which is the point: this changes the entropy source, not the contract. No new test was added, since the property that matters (uniform over 1..=16, 16 reachable) is already covered and a second test asserting the same thing would be redundant.
  • cargo clippy --workspace --all-targets does not complete in this environment: an unrelated crate's build script needs alsa.pc and the system package is absent. Full matrix left to CI, where Clippy Linter, Build & Lint (all features) and Feature Matrix are green.

CI is green apart from Semver Checks (informational), which already fails on main at this PR's base commit 02474521e from waproto drift against the last published release.

random_pad_len seeded a fresh StdRng to produce a single byte: 32 bytes
of seed pulled from the thread generator, a full ChaCha12 key schedule
and a 256-byte block generated, all so it could return (x & 0x0F) + 1.
Use the thread-local generator directly. It is the same already-seeded
ChaCha12 CSPRNG, so this is not a weakening of the entropy source, and
the distribution is untouched.

The identical fix already exists in request.rs for message_id_at; it was
never applied here.

Measured on a pinned core: 759 -> 41 retired instructions per call under
callgrind, 99.1 -> 3.3 ns per call. The pad is drawn twice per DM send.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

MessageUtils::random_pad_len() now uses the thread-local random generator while preserving the existing 1..=16 padding-length distribution.

Changes

Padding randomness

Layer / File(s) Summary
Update padding-length random source
wacore/src/messages.rs
random_pad_len() switches from a newly constructed StdRng to rand::rng() while retaining the existing byte generation and 1..=16 mapping.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

Suggested labels: performance

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 performance change in MessageUtils::random_pad_len.
Description check ✅ Passed The description is directly related to the code change and its validation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/random-pad-len-csprng-utgo78

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.

@github-actions

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.10 MiB 10.10 MiB -192 B (-0.00%) 🔽
bin .text 8.13 MiB 8.13 MiB -256 B (-0.00%) 🔽
bin allocated (text+data+bss) 10.10 MiB 10.10 MiB -32 B (-0.00%) 🔽
llvm-lines wacore 509,758 509,763 +5 (+0.00%) 🔺
llvm-lines wacore copies 16,636 16,636 0
llvm-lines whatsapp-rust lib 723,789 723,789 0
llvm-lines whatsapp-rust lib copies 22,819 22,819 0
deps crates (Cargo.lock) 471 471 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.84 MiB 1.84 MiB -832 B (-0.04%) 🔽
.text wacore 692.34 KiB 692.55 KiB +224 B (+0.03%) 🔺
.text wacore_binary 91.40 KiB 91.40 KiB 0
.text wacore_libsignal 166.27 KiB 166.27 KiB 0
.text wacore_appstate 22.35 KiB 22.35 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.98 KiB 515.98 KiB 0
.text whatsapp_rust_tokio_transport 40.36 KiB 40.36 KiB 0
.text whatsapp_rust_ureq_http_client 10.33 KiB 10.33 KiB 0
.text std 1.08 MiB 1.08 MiB 0
.text other deps 1.91 MiB 1.91 MiB +312 B (+0.02%) 🔺

Baseline: 02474521e (latest main run) · Head: d92571c45 · Graphs

@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@codspeedbot explain why this not appears as faster

@codspeed-hq

codspeed-hq Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@codspeedbot explain why this not appears as faster

Short answer: CodSpeed did measure it. Nothing was badged as an improvement because the effect is only a meaningful fraction of one benchmark in the suite, and even there it stayed under the threshold this repo flags at.

Comparing this PR's run (3fa980d) against the main baseline (0247452), Simulation instrument, headline metric:

Benchmark main PR Δ
bench_dm_recv_steady (control, untouched) 67.159 µs 67.166 µs +0.01%
bench_encode_and_pad 16.42 µs 14.99 µs −8.7%
bench_dm_send_encode_work[text_reply] 57.82 µs 57.13 µs −1.2%
bench_dm_send 144.19 µs 143.85 µs −0.24%

The flamegraphs make it unambiguous. On main, bench_encode_and_pad contains rand::make_rng::<StdRng> at 811 ns (4.94% of the benchmark) plus chacha20::ChaChaCore<R12>::generate at 627 ns (3.82%) — 1.44 µs, 8.8% of the benchmark. On the PR neither frame exists; random_pad_len is a single 216 ns (1.44%) leaf. The 1.44 µs that vanished is essentially the entire 1.43 µs the benchmark dropped by.

The instruction sub-component behaves exactly as your callgrind work predicted, and it is per-call additive: bench_encode_and_pad (one pad draw) −207 ns, bench_dm_send_encode_work[text_reply] (two draws) −435 ns, bench_dm_send −244 ns of 79.5 µs. bench_encode_and_pad on the last four main runs reads 16.16 / 16.16 / 16.42 / 16.42 µs, so 14.99 µs is well outside baseline drift.

So why no badge:

  1. Denominator. Your "roughly 0.2% of a DM send" is what the send benches show: −0.24% on bench_dm_send. That is real and, unlike your local run, resolvable — the control moved 0.01% here versus the ~11% spread you saw on wall clock — but it is nowhere near a reportable delta.
  2. Threshold. The one benchmark where the pad draw is a real share, bench_encode_and_pad, comes out at +9.6% faster in CodSpeed's change metric, and that still landed in the Unchanged bucket for this comparison — it did not clear the threshold configured for this repository. For calibration, the bench(send): measure the shapes production actually sends #1186 fixture change (bench_dm_send 86.4 → 144.2 µs) was flagged. If you want single-digit-percent moves on the micro-benches to be badged, lower the threshold in the repo's CodSpeed settings; the data to badge is already there.
  3. The run-level impact figure aggregates all ~210 benchmark/instrument pairs, so a 1.4 µs win on one micro-bench averages to nothing. It also mixes in sub-percent runner drift: the base ran on AMD EPYC 7763 and this PR's wacore shard on an Intel Xeon 8573C. Per-benchmark the deterministic instrument shrugged that off (control at +0.01%), so it does not affect the conclusions above — only the aggregate headline.

The Memory instrument cannot show anything either way: seeding StdRng is stack-only, so allocation counts are unchanged (bench_encode_and_pad reports 122 B vs 119 B, which is just the random pad length landing on a different value between runs).

Net: the report reading "no change" is the suite telling you the win is 0.2% of a send, which is what the PR description already claims. bench_encode_and_pad is the benchmark that guards this code, and it moved −8.7%.

Summary
  • Warming up the performance wizard...
  • Investigating CodSpeed results for PR perf(messages): draw the pad length from the thread RNG #1189 — Compared the PR run against the main baseline, inspected per-instrument metrics and the base/head flamegraphs for the padding path, and identified why no benchmark was badged as an improvement.
  • Handling comment — Comment handling finished successfully

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

Copy link
Copy Markdown
Collaborator Author

Adding the local numbers while the bot digs, since they explain it without needing the flamegraphs.

The change removes ~719 retired instructions per call under callgrind (759.5 -> 40.7, taken as the slope between a 1k and a 101k call run so process startup cancels). The pad is drawn twice per DM send, so ~1438 instructions per bench_dm_send iteration. I confirmed that independently by running callgrind over the before/after bench binaries: the whole-process slope differs by 1439 instructions per iteration, which is the same number arriving from the other direction.

Against the timed region that is small. bench_dm_send's fastest sample on my box is about 10.2 us and the pad work is about 190 ns of it, so roughly 1.9%. And it is tight cache-resident ChaCha, which is exactly the kind of work a simulated cycle estimate values least: the estimate is dominated by the cache behavior of the surrounding Signal and proto work, so the modeled saving is smaller than the instruction saving.

The other half is that base and head did not run on the same hardware. The compare for this run lists bench_dm_send under Environment Differences, base on AMD EPYC 7763 and head on Intel Xeon Platinum 8573C, for both Simulation and Memory. A sub-percent delta is not going to survive a change in the simulated cache model.

Overall run impact came out at -0.14%: right direction, too small to badge. That matches what the PR body claims. It is worth taking because it deletes pure work at zero risk, not because 190 ns per send is going to be visible anywhere.


Generated by Claude Code

@jlucaso1
jlucaso1 merged commit 535b5fd into main Jul 29, 2026
22 of 23 checks passed
@jlucaso1
jlucaso1 deleted the claude/random-pad-len-csprng-utgo78 branch July 29, 2026 14:47
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.

2 participants