Skip to content

perf(group): keep warm sends warm under the own-device SKDM steady state - #1021

Merged
jlucaso1 merged 3 commits into
mainfrom
perf/own-skdm-warm-path
Jul 11, 2026
Merged

perf(group): keep warm sends warm under the own-device SKDM steady state#1021
jlucaso1 merged 3 commits into
mainfrom
perf/own-skdm-warm-path

Conversation

@jlucaso1

Copy link
Copy Markdown
Collaborator

Summary

Since #999, own devices are never memoized warm (WA Web's !isMeDevice guard on markHasSenderKey), so every warm group send resolves a non-empty needs_skdm set — and that tripped the cold-send machinery on every send: take the per-group distribution lock, invalidate the device-map cache, re-read it from SQLite (from_db_rows) and resolve targets a second time. The symbolic DHAT profile showed ~430 of 500 warm group sends going through that path.

Verified against the captured WA Web bundles that none of this has a counterpart there: ParticipantStore.getGroupSenderKeyList computes skDistribList/skList from the in-memory participant map with no storage re-read (own devices sit at false forever and re-enter the distrib list each send), and GroupSkmsgJob only calls markHasSenderKey after the ack — whose mutation skips own devices. Wire behavior is unchanged by this PR: own devices still re-receive their SKDM on every send, external members still take the cold flow.

Three changes:

  • send_group_branch treats an own-devices-only needs set as the warm steady state: no distribution guard, no cache invalidation, no re-resolve.
  • update_sender_key_devices drops its unconditional cache invalidation — set_sender_key_status_for_devices already invalidates when it actually writes a new warm mark, and an own-only set writes nothing.
  • skdm_warm_memo stores the memoized needs set (empty or own-only), reviving the warm fast path: it had been dead since needs stopped ever being empty, so the O(devices) filter ran on every send.

Besides allocations, this removes one SQLite read from every warm group send and stops the per-group distribution lock from serializing warm sends.

Impact

DHAT group-send A/B (16 members, 500 cycles, 2 runs per side, baseline = main @ 2591b24): -12.1% bytes and -23.2% blocks per cycle (45,584 → 40,068 B; 319.5 → 245.3 blocks). DM ping-pong sanity run shows no regression. 500/500 pongs with 0 lost on both sides — own-device SKDM distribution still flows.

Validation

  • cargo test -p whatsapp-rust --lib (978 passed; new tests: an own-only SKDM mark keeps the device map cached while an external warm mark still invalidates it, plus the own-only classification gate incl. the empty/external cases)
  • cargo clippy -p whatsapp-rust -p wacore --lib --tests -- -D warnings
  • RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo check -p whatsapp-rust --lib --target wasm32-unknown-unknown --no-default-features
  • Cross-checked against docs/captured-js: WAWeb/Api/ParticipantStore.js (isMeDevice guard in the mark mutation; in-memory getGroupSenderKeyList) and WAWeb/Send/GroupSkmsgJob.js (markHasSenderKey after ack only)

Since own devices are never memoized warm (WA Web's !isMeDevice guard on
markHasSenderKey), every warm group send resolves a non-empty needs_skdm
set — and that tripped the cold-send machinery on every send: take the
group distribution lock, invalidate the device map cache, re-read it
from SQLite and resolve targets a second time. WA Web has no per-send
counterpart to any of this: getGroupSenderKeyList reads the in-memory
participant map and markHasSenderKey mutates it after the ack.

Three changes, wire behavior unchanged (own devices still re-receive
their SKDM on every send):

- send_group_branch treats an own-devices-only needs set as the warm
  steady state: no distribution guard, no invalidation, no re-resolve.
- update_sender_key_devices drops its unconditional cache invalidation;
  set_sender_key_status_for_devices already invalidates when it actually
  writes a new warm mark, and an own-only set writes nothing.
- skdm_warm_memo stores the memoized needs set (empty or own-only) so
  the warm fast path works again — it had been dead since the needs set
  stopped ever being empty.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved SKDM handling when needs targets only the sender’s own devices, treating it as a warm steady state to avoid unnecessary re-resolve and redistribution.
    • Enhanced warm memo reuse so memoized needs are returned on matching identity/generation, rather than falling back to a cold path.
    • Adjusted sender-key device map cache invalidation to occur only on write failure (preserving warm marks after successful updates).
  • Tests

    • Added tests for “own-devices-only” SKDM classification (own-only, external-member, and empty needs).
    • Added tests ensuring own-devices-only behavior does not invalidate the cached sender-key device map.

Walkthrough

The client warm memo now stores memoized SKDM needs. Sender logic recognizes own-device-only needs, preserves warm memoization, limits sender-key device cache invalidation to failures, and tests classification and cache retention.

Changes

SKDM warm-path refinement

Layer / File(s) Summary
Memo contract and own-device classification
src/client.rs, src/send/mod.rs
The warm memo uses SkdmWarmMemoEntry, and skdm_needs_only_own_devices identifies non-empty needs belonging only to the sender’s PN/LID identities.
Warm resolution and cache retention
src/send/mod.rs
Memo hits return retained needs; own-only needs remain on the warm path, successful sender-key updates preserve the cache, and tests cover classification and cache retention.

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

Sequence Diagram(s)

sequenceDiagram
  participant send_group_branch
  participant resolve_skdm_targets_memoized
  participant Client
  participant SenderKeyDeviceMap
  send_group_branch->>resolve_skdm_targets_memoized: resolve SKDM targets
  resolve_skdm_targets_memoized->>Client: read skdm_warm_memo
  Client-->>resolve_skdm_targets_memoized: memoized needs
  resolve_skdm_targets_memoized->>SenderKeyDeviceMap: classify own-device-only needs
  SenderKeyDeviceMap-->>send_group_branch: own-only needs
  send_group_branch-->>send_group_branch: retain warm send path
Loading

Possibly related PRs

Suggested labels: performance

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

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main optimization: keeping own-device SKDM warm sends on the fast path.
Description check ✅ Passed The description is directly related to the changes and accurately explains the warm-send optimization and cache behavior.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/own-skdm-warm-path

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 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR improves the warm group-send SKDM path. The main changes are:

  • Treats own-device-only SKDM targets as the warm steady state.
  • Memoizes warm SKDM target sets with the exact sending identity.
  • Avoids sender-key device cache invalidation when own-only marks write nothing.
  • Invalidates the sender-key device cache when a warm-mark write fails.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.
  • The memo reuse path now checks the sending identity before reusing targets.
  • The warm-mark failure path now drops the cached sender-key map after a failed write.

Important Files Changed

Filename Overview
src/client.rs Adds a typed SKDM warm memo entry that includes the device/map pair, generation, sending identity, and memoized targets.
src/send/mod.rs Updates SKDM warm-path classification, memo reuse, distribution lock handling, and sender-key cache invalidation behavior.

Reviews (3): Last reviewed commit: "fix(group): read the map generation afte..." | Re-trigger Greptile

Comment thread src/send/mod.rs
Comment thread src/send/mod.rs Outdated

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client.rs`:
- Around line 150-159: The SkdmWarmMemoEntry key omits the exact sending
identity, allowing stale memoized targets when own_sending_jid changes. Add a
Jid field to SkdmWarmMemoEntry, store own_sending_jid.clone() when creating
entries, and require exact sender equality on every memo hit in the warm-memo
lookup logic. Add a regression test covering a sender-device change while the
resolved-device Arc remains reusable.

In `@src/send/mod.rs`:
- Around line 1748-1756: Update the post-re-resolution handling in the send flow
near the cold-branch logic so that when the newly resolved needs satisfy
skdm_needs_only_own_devices(&needs, Some(own_jid), Some(own_lid)), the
distribution_guard is released before the network send. Apply the same own-only
warm classification used in the initial match, avoiding cache invalidation,
distribution guarding, and re-resolution for this case.
🪄 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: 18aab5a8-c0ea-4d27-876c-aba7281e4904

📥 Commits

Reviewing files that changed from the base of the PR and between 2591b24 and 478c011.

📒 Files selected for processing (2)
  • src/client.rs
  • src/send/mod.rs

Comment thread src/client.rs
Comment thread src/send/mod.rs
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.85 MiB 10.85 MiB +5.81 KiB (+0.05%) 🔺
bin .text 8.85 MiB 8.85 MiB +5.31 KiB (+0.06%) 🔺
bin allocated (text+data+bss) 10.85 MiB 10.85 MiB +3.95 KiB (+0.04%) 🔺
llvm-lines wacore 505,737 505,737 0
llvm-lines wacore copies 17,371 17,371 0
llvm-lines whatsapp-rust lib 771,437 771,719 +282 (+0.04%) 🔺
llvm-lines whatsapp-rust lib copies 25,062 25,065 +3 (+0.01%) 🔺
deps crates (Cargo.lock) 472 472 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.67 MiB 1.67 MiB +4.65 KiB (+0.27%) 🔺
.text wacore 527.79 KiB 527.79 KiB 0
.text wacore_binary 148.45 KiB 148.45 KiB 0
.text wacore_libsignal 179.42 KiB 179.42 KiB 0
.text wacore_appstate 158.25 KiB 158.25 KiB 0
.text wacore_noise 26.05 KiB 26.05 KiB 0
.text waproto 1.60 MiB 1.60 MiB 0
.text whatsapp_rust_sqlite_storage 513.00 KiB 513.00 KiB 0
.text whatsapp_rust_tokio_transport 43.79 KiB 43.79 KiB 0
.text whatsapp_rust_ureq_http_client 10.47 KiB 10.47 KiB 0
.text std 1.01 MiB 1.01 MiB +585 B (+0.06%) 🔺
.text other deps 2.95 MiB 2.95 MiB +34 B (+0.00%) 🔺
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust 1.67 MiB 1.67 MiB +4.65 KiB (+0.27%)
rustix 191 B 1.88 KiB +1.69 KiB (+908.38%)
buffa_descriptor 4.67 KiB 2.98 KiB -1.69 KiB (-36.25%)

Baseline: 2591b246c (latest main run) · Head: 9a5381100 · Graphs

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 2 files

Confidence score: 5/5

  • Safe to merge after the addressed issues were fixed.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/send/mod.rs Outdated
Comment thread src/send/mod.rs
- release the distribution guard when the cold-path re-resolve lands on
  the own-only steady state, mirroring the first-resolution arm
- key the warm memo on the exact sending identity, so a mid-session
  identity change (e.g. LID discovery) can't reuse needs computed for
  the old sender
- invalidate the device-map cache when the warm-mark write errors: a
  failed write may have partially landed and backends are not required
  to be atomic
greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 11, 2026

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/send/mod.rs
A cold flip during the awaited device resolve advanced the map's
generation, but both sides of the memo comparison still held the
pre-flip value, so the stale memoized needs could be reused for one
send. Loading the generation after the resolve (still before the
filter, so a flip racing the filter stamps the inserted memo stale)
shrinks the window to the same bounded one the unmemoized filter has.
@greptile-apps
greptile-apps Bot dismissed their stale review July 11, 2026 01:22

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@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 (2)
src/send/mod.rs (2)

3187-3203: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Verify that the external warm mark actually succeeded.

Both success and failure invalidate this cache, so the test passes even if persistence always fails. Assert the member was stored as warm before checking cache rebuilding.

Proposed test hardening
         client
-            .update_sender_key_devices(group, &[own_primary, member])
+            .update_sender_key_devices(group, &[own_primary, member.clone()])
             .await;
+        let rows = client
+            .persistence_manager
+            .get_sender_key_devices(group)
+            .await
+            .unwrap();
+        assert!(
+            rows.iter()
+                .any(|(jid, has_key)| jid == &member.to_string() && *has_key),
+            "the external warm mark must persist successfully"
+        );
🤖 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 `@src/send/mod.rs` around lines 3187 - 3203, Strengthen the test around the
external warm mark by verifying persistence succeeded before testing cache
invalidation. After update_sender_key_devices, query the stored sender-key
device state for member and assert it is marked warm, then retain the existing
get_or_init and rebuilt assertion.

1155-1180: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Read the generation after the awaited memo lookup.

skdm_warm_memo.get(group).await can suspend after cached_map_gen is captured. A cold flip during that await increments the map generation, but the stale local value still permits returning an outdated memo and omitting the newly cold target for this send.

Proposed fix
-                let cached_map_gen = cached_map.generation();
+                let warm_memo = if self.group_devices_memo_enabled {
+                    self.skdm_warm_memo.get(group).await
+                } else {
+                    None
+                };
+                let cached_map_gen = cached_map.generation();
...
-                if self.group_devices_memo_enabled
-                    && let Some((dw, cw, memo_gen, memo_sender, memo_needs)) =
-                        self.skdm_warm_memo.get(group).await
+                if let Some((dw, cw, memo_gen, memo_sender, memo_needs)) = warm_memo
                     && std::ptr::eq(dw.as_ptr(), std::sync::Arc::as_ptr(&all_devices))
🤖 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 `@src/send/mod.rs` around lines 1155 - 1180, Refresh the cached map generation
after the awaited skdm_warm_memo.get(group) lookup and use that refreshed value
for the memo_gen comparison in the fast path. Update the logic around
skdm_warm_memo.get and cached_map_gen so a generation change during the await
prevents returning stale memoized targets.
🤖 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.

Outside diff comments:
In `@src/send/mod.rs`:
- Around line 3187-3203: Strengthen the test around the external warm mark by
verifying persistence succeeded before testing cache invalidation. After
update_sender_key_devices, query the stored sender-key device state for member
and assert it is marked warm, then retain the existing get_or_init and rebuilt
assertion.
- Around line 1155-1180: Refresh the cached map generation after the awaited
skdm_warm_memo.get(group) lookup and use that refreshed value for the memo_gen
comparison in the fast path. Update the logic around skdm_warm_memo.get and
cached_map_gen so a generation change during the await prevents returning stale
memoized targets.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 90feaf5f-ba77-494a-82c2-cdfb5056a43a

📥 Commits

Reviewing files that changed from the base of the PR and between 8e92316 and 4e4c97a.

📒 Files selected for processing (1)
  • src/send/mod.rs

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 1 file (changes from recent commits).

Requires human review: Optimizes group send cache; modifies core sending logic.

Re-trigger cubic

@jlucaso1
jlucaso1 merged commit d9e693f into main Jul 11, 2026
23 checks passed
@jlucaso1
jlucaso1 deleted the perf/own-skdm-warm-path branch July 11, 2026 01:33
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