diag(send): measure which term invalidates each group-path device memo - #1292
Conversation
Two benchmark PRs (#1283, #1285) bounded the cost of a device-memo hit and of a miss, and both had to force the outcome to do it. Neither could say which outcome a client in regime takes, and that is what decides whether the miss-path cost is a bill anyone pays. This adds the counters that answer it, per term rather than per memo. `Client::device_memo_stats()` reports, for each call to `resolve_group_devices_memoized` and `resolve_skdm_targets_memoized`, which validity term decided it: three terms plus a scoped re-stamp for the group memo, four for the SKDM memo, plus the not-stored case that makes the next call miss by construction. An aggregate miss count cannot tell those apart, and it cannot separate cause from consequence either — the SKDM memo compares the Arc the group memo returned. `skdm_memo_entry_is_valid` becomes `skdm_memo_entry_stale_term`, same terms in the same short-circuit order, now naming the first that failed. No memo semantics change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uk5eo5FdJfBQeMQBJWRo8
Classifying into an outcome value and then dispatching on it a second time to act cost 126 instructions per resolve, measured; recording on the branch that decided cuts that to 5. The SKDM half also stopped moving the memo entry — an owned clone of a five-field tuple carrying a Jid and a Vec<Jid> — into a temporary just to classify it first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uk5eo5FdJfBQeMQBJWRo8
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uk5eo5FdJfBQeMQBJWRo8
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR adds always-on statistics for group-device and SKDM memo resolution. Memo paths record typed hit, miss, restamp, bypass, and non-stored outcomes. It also adds PN/LID validation tests, benchmark access, and observability documentation. ChangesDevice memo observability
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant GroupSendFixture
participant SKDMMemo
participant DeviceRegistry
participant DeviceMemoCounters
GroupSendFixture->>SKDMMemo: resolve memoized targets
SKDMMemo->>DeviceRegistry: resolve group devices
DeviceRegistry-->>SKDMMemo: return group memo outcome
SKDMMemo->>DeviceMemoCounters: record SKDM outcome
DeviceMemoCounters-->>GroupSendFixture: provide memo statistics
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
|
| Filename | Overview |
|---|---|
| src/client/device_memo_stats.rs | Introduces per-client group-device and SKDM outcome counters, snapshots, deltas, rates, and formatting with exhaustive enum-to-slot compile-time checks. |
| src/send/mod.rs | Records each SKDM lookup outcome, including prerequisite resolution failures, and expands PN/LID memo behavior tests. |
| src/client/device_registry.rs | Adds branch-local accounting for group-device memo hits, re-stamps, misses, and bypasses without changing resolver decisions. |
| src/client.rs | Registers the new statistics module, exports its public reporting types, and stores counters on each client. |
| src/client/lifecycle.rs | Initializes the per-client memo counters during client construction. |
| src/bench_support.rs | Exposes memo snapshots through the group-send benchmark harness. |
| benches/client_group_send.rs | Updates benchmark guidance to explain where memo outcome frequency is measured. |
| agent_docs/observability.md | Documents the new always-on memo statistics surface and interpretation of each outcome. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Resolve SKDM targets] --> B[Resolve group devices]
B -->|Error| C[Record resolve_failed]
B -->|Success| D{SKDM memo enabled?}
D -->|No| E[Record bypassed]
D -->|Yes| F{Memo entry state}
F -->|Absent| G[Record miss_absent]
F -->|Stale| H[Record first stale term]
F -->|Valid| I[Record hit and return memoized targets]
E --> J[Filter SKDM targets]
G --> J
H --> J
J --> K{Targets memoizable?}
K -->|Yes| L[Store memo]
K -->|No| M[Record not_stored]
L --> N[Return resolved targets]
M --> N
C --> O[Return None]
Reviews (4): Last reviewed commit: "docs(perf): describe hit_rate as memo hi..." | Re-trigger Greptile
There was a problem hiding this comment.
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/bench_support.rs`:
- Around line 191-193: Update the documentation near Self::warm_send to replace
the unclear phrase “which outcome a client in regime takes” with wording that
states “which outcome a client takes in a given regime,” preserving the
surrounding benchmark guidance.
In `@src/client/device_memo_stats.rs`:
- Around line 144-149: Replace the last-variant assertions in the const block
with exhaustive const-evaluated matches for GroupDevicesMemoOutcome and
SkdmTargetsMemoOutcome, assigning every enum variant its expected array index.
Ensure adding any variant requires updating the match and fails compilation if
omitted, while leaving the as usize indexing in record_group_devices and
record_* unchanged.
In `@src/send/mod.rs`:
- Line 3613: Update the string literal passed to
fixture.client.device_topology.record in the topology fixture from the invalid
555 NPA format to the repository-standard fictional NANP value 12025550111,
preserving its use as the topology cache key.
🪄 Autofix
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: 9ada4112-ba5f-4ed1-a044-bda5dd72db4b
📒 Files selected for processing (8)
agent_docs/observability.mdbenches/client_group_send.rssrc/bench_support.rssrc/client.rssrc/client/device_memo_stats.rssrc/client/device_registry.rssrc/client/lifecycle.rssrc/send/mod.rs
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 331aaa5c4a
ℹ️ 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".
| // Nothing stored, so the next call is a MissAbsent by | ||
| // construction rather than by eviction. Counted apart | ||
| // so that distinction survives into the report. | ||
| self.device_memo_counters.record_skdm_not_stored(); |
There was a problem hiding this comment.
Clear stale entries before recording not_stored
When an existing memo becomes stale and the newly resolved targets contain a non-own device, this branch records not_stored but leaves the old entry in skdm_warm_memo. The next lookup therefore sees Some(memo) and reports the applicable stale term (the forget-path test already demonstrates MissMapGeneration followed by MissMap), not the promised miss_absent; this makes the public diagnostic misleading when consumers use not_stored to distinguish deliberate non-storage from eviction. Remove the stale entry here or revise the counter's documented semantics.
AGENTS.md reference: AGENTS.md:L59-L59
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Right that the doc overpromised; fixed there rather than in the code, because clearing the entry buys nothing.
A stale entry left in place cannot come back to life. The map generation is monotonic, the map Arc is replaced wholesale on a rebuild, and the device-set Weak keeps the old allocation alive precisely so a new Arc can never land on the same address — that is the ABA guard GroupDevicesMemo documents. So the entry stays stale until something overwrites it, and the failure mode a clear would prevent (serving a stale needs_skdm) is not reachable. Clearing would add a cache write on the miss path and change no outcome.
What was wrong was the claim, so not_stored now documents what it actually guarantees — the next call cannot hit — and says explicitly that when a stale entry was already there the next call reports whichever term is still failing, not miss_absent. Your read of the forget-path test is exactly that case, and it is now the example in the comment. SkdmTargetsMemoOutcome::MissAbsent lost the same wrong sentence.
Your sibling comment about the Err arm was a real hole in the API contract and is fixed as you described: SkdmTargetsMemoOutcome::ResolveFailed is recorded there, so calls() is one per call again and hit_rate() cannot look healthy over a denominator that quietly shrank.
Generated by Claude Code
| if !self.device_memos_enabled { | ||
| self.device_memo_counters | ||
| .record_skdm_targets(Outcome::Bypassed); |
There was a problem hiding this comment.
Account for failed SKDM resolver calls
When resolve_group_devices_memoized returns an error—for example after a failed cache-backed device resolution—execution reaches the outer Err arm before any SKDM outcome is recorded, because all new counters are inside the success arm. Consequently SkdmTargetsMemoStats::calls() is not one counter per resolver call as its public API claims, and hit_rate() can appear healthy while attempted group sends are failing before memo lookup; add an error/aborted outcome or explicitly scope the API to successful prerequisite resolutions.
AGENTS.md reference: AGENTS.md:L59-L59
Useful? React with 👍 / 👎.
📦 Binary size report
.text per crate
Baseline: |
Four review findings, all valid: - The const assert only pinned the last variant's index, so a variant added after it would keep the assert passing and index off the end of the array on the send path. Replaced with exhaustive const `match`es, which stop compiling when a variant is added anywhere. `record_*` still indexes by `as usize`, so the hot path is unchanged. - A resolver call whose device resolution failed recorded no SKDM outcome, so `calls()` was not one per call and `hit_rate()` could look healthy over a shrinking denominator. Added `resolve_failed`. - `not_stored` promised the next call would be `miss_absent`. It does not: a stale entry is left in place, so the next call reports whichever term is still failing. Documented what it actually guarantees — that the next call cannot hit — and why leaving the stale entry is correct. - The unrelated-user literal used an NPA of 555, which is not the reserved fictional NANP format. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uk5eo5FdJfBQeMQBJWRo8
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b63882f231
ℹ️ 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".
| own-devices-only, so nothing was memoized and the *next* call is a | ||
| `miss_absent` by construction, not by eviction). |
There was a problem hiding this comment.
Stop promising an absent miss after non-storage
When a stale SKDM memo already exists and a newly resolved non-own target set cannot be stored, the implementation deliberately retains that stale entry, so the next lookup reports whichever stale term still fails rather than miss_absent. Fresh evidence after the earlier review is that the updated SkdmTargetsMemoStats::not_stored documentation now explicitly describes this behavior, but this observability guide still promises the opposite, which can cause operators to misclassify deliberate non-storage as eviction; update this guide to match the public counter semantics.
AGENTS.md reference: AGENTS.md:L59-L59
Useful? React with 👍 / 👎.
The counter's own doc was fixed last commit; this guide still promised the next call would be `miss_absent`, which would have operators reading a run of `not_stored` as eviction pressure instead of a group that never settles. Also documents `resolve_failed`, which the same round added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uk5eo5FdJfBQeMQBJWRo8
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
agent_docs/observability.md (1)
114-122: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winReconcile the SKDM term count with
miss_absent.
DeviceMemoStatsexposes five SKDM miss counters:miss_absent,miss_devices,miss_map,miss_map_generation, andmiss_sender. This section lists only four SKDM validity terms and omits the absent-entry condition, while it explicitly includesentry presentfor group memos. State that the SKDM memo has four stale terms plus the entry-absent condition, or list all five conditions.Proposed documentation fix
- and the SKDM memo has four - (device `Arc`, sender-key-map `Arc`, map generation, sending identity). + and the SKDM memo has four stale terms + (device `Arc`, sender-key-map `Arc`, map generation, sending identity), + plus the entry-absent condition recorded by `miss_absent`.🤖 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 `@agent_docs/observability.md` around lines 114 - 122, Update the SKDM memo description to account for all five miss counters: four stale-validity terms (device Arc, sender-key-map Arc, map generation, and sending identity) plus the entry-absent condition. Keep the existing group memo terms and explanation of dependent miss_devices behavior unchanged.
🤖 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 `@agent_docs/observability.md`:
- Around line 114-122: Update the SKDM memo description to account for all five
miss counters: four stale-validity terms (device Arc, sender-key-map Arc, map
generation, and sending identity) plus the entry-absent condition. Keep the
existing group memo terms and explanation of dependent miss_devices behavior
unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2229b96c-f2d3-4a60-88b7-0d8e87eefbcb
📒 Files selected for processing (1)
agent_docs/observability.md
The four stale terms are not the whole story: the entry-absent condition is a fifth thing the report distinguishes, and the group half already listed "entry present" as one of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uk5eo5FdJfBQeMQBJWRo8
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 274e9e732b
ℹ️ 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".
| /// Share of calls that skipped `filter_skdm_targets`. `None` when nothing | ||
| /// was resolved yet. | ||
| pub fn hit_rate(&self) -> Option<f64> { | ||
| let calls = self.calls(); | ||
| (calls > 0).then(|| self.hits as f64 / calls as f64) |
There was a problem hiding this comment.
Describe hit_rate as the memo-hit share
When prerequisite device resolution fails, the call never reaches filter_skdm_targets, but resolve_failed is included in calls() and excluded from the numerator. Therefore this method does not return the documented share of calls that skipped the filter; it returns memo hits divided by all resolver attempts. In failure-heavy workloads, consumers relying on this description will misinterpret the metric, so document the failure-inclusive memo-hit semantics instead.
AGENTS.md reference: AGENTS.md:L59-L59
Useful? React with 👍 / 👎.
Adding `resolve_failed` to the denominator made the old wording — "share of calls that skipped filter_skdm_targets" — wrong: a call that errored before the memo lookup skipped the filter too. Keeping those in the denominator is the point, so the rate sags when group sends start failing instead of climbing; the doc now says that rather than the opposite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uk5eo5FdJfBQeMQBJWRo8
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
Summary
PRs #1283 and #1285 bounded the cost of a device-memo hit and of a memo miss, and both had to force the outcome to do it. Neither could say which outcome a client takes once the group is warm. An external profile of a client at
763aea9implied the answer was "miss, on all 30 of 30 sends", withresolve_skdm_targets_memoizedat 117,483 Ir/send at 512 members — ~10% of that process's profiled instructions.Measured here, the answer is the opposite: 30 hits out of 30, on both memos, at 8, 32, 128 and 512 members.
filter_skdm_targetscosts 0 Ir/send at 512 because it never runs, andresolve_group_devices_uncachedcosts 1. The external profile is not reproducible against this code, and the batch closes as a characterization: nothing was optimized, and nothing needed to be.The deliverable is the instrumentation that made that answerable, and it is per-term on purpose. The group memo has three validity terms plus a scoped re-stamp, the SKDM memo has four stale terms plus an entry-absent condition; an aggregate miss count cannot tell a metadata refresh from an in-place cold flip from an entry that was never stored, and it cannot separate cause from consequence either — the SKDM memo compares the
Arcthe group memo returned, so a group-memo recompute forcesskdm_targets.miss_devicesregardless of its own terms.Numbers below are from this container, not from CodSpeed and not from the machine that produced the external profile. Absolute values are not comparable across machines; every comparison here is against a baseline measured on the same host, in the same session.
Changes
src/client/device_memo_stats.rs(new) —Client::device_memo_stats() -> DeviceMemoStats, always on, no feature gate, peragent_docs/observability.md's rule. Per-term counts for both resolvers, plusDeviceMemoStats::sincefor scoping a window without a reset (a reset would race a concurrent send). Counters are two arrays indexed by the outcome discriminant, so recording is one indexed relaxedfetch_addwith no branch on the variant; exhaustiveconst fnslot maps tie the array lengths to the enums, so adding a variant anywhere — including after the current last one — stops the crate compiling instead of indexing off the end on the send path. Not#[cfg(test)]likedm_devices_memo_recomputes: a test counter answers the question in a fixture, and the question is what a deployed client gets.skdm_memo_entry_is_valid→skdm_memo_entry_stale_term— same four terms, same short-circuit order, now naming the first that failed instead of returningbool. No memo semantics change; the test helper calls the same predicate as before.resolve_group_devices_memoized— each exit records its own term. No change to which branch is taken.src/send/mod.rs— the regime scenario in PN and LID mode, the server-paced variant, and one that drives each miss term and checks it is reported as itself.GroupSendFixturegains a LID mode — participants LID-addressed,lid_to_pn_mappopulated, pairs durably in the LID-PN cache. PR bench(client): measure the group send the client crate actually pays #1283 named LID as the largest gap in its coverage, and it is the mode the external profile ran.bench_support:GroupSendHarness::memo_stats()— one accessor, so the hit rate is readable at group sizes no unit test builds. No new benchmark target and no new CodSpeed work: the existing shard already carries four 512-member fixtures, and a hit rate is not a thing a benchmark should measure.Cost
Method as in #1283: callgrind (valgrind 3.22) over a driver built from the committed
bench_supportfixture, per-iterationIr = (Ir at K − Ir at 1) / (K − 1), minimum of the repetitions, spread stated.K = 10001for the resolver (it does not advance the sender-key chain, so the rotation threshold does not bound it) — at 512 members the setup is 2.78G Ir and drifts ~100K between runs, which at K=1001 is ±200 Ir of noise on a 4,400 Ir measurement and at K=10001 is ±20. Baseline is139b315, this branch's base at the time, rebuilt and measured in the same session.The hit rate — the number this batch exists for
30 consecutive warm sends per group size, after one cold send and one priming warm send, counters read over the window:
Not one miss, of any term, at any size.
not_storedis 0 too, so no send left the next one unable to hit.What a warm send at 512 members actually pays (baseline build, inclusive Ir/send)
resolve_skdm_targets_memoizedresolve_group_devices_uncachedfilter_skdm_targetschainget_user_devices_owned(per-member fan-out)set_sender_key_status_for_devicesGroupInfo::phone_jid_for_lid_userThe three zeros are the finding: they are what a memo hit is.
filter_skdm_targetsdoes not appear because the SKDM memo short-circuits before it;resolve_group_devices_uncachedandget_user_devices_owneddo not appear because the group memo short-circuits before them.The memo-hit resolve is flat, with the counters in
Spread ≤ 21 Ir at every size. #1285's shape reproduces; the absolute level differs because the machine does.
What the counters cost
139b315+16 Ir on the tightest thing the counters sit inside; at whole-send scale the effect is not separable from run-to-run noise. Two relaxed
fetch_adds per send. Binary size: +576 B (+0.01%), per the size gate on this PR. CodSpeed flags no regression on any of the four shards.One thing worth recording, because the first revision got it wrong by 8×. Classifying the outcome into an enum and then matching on it a second time to act cost 126 Ir per resolve, not 16 — the second dispatch, plus moving the SKDM memo entry (an owned clone of a five-field tuple carrying a
Jidand aVec<Jid>) into a temporary just to classify it first. Recording on the branch that decided cut it to 16. Measured both ways, on the same host, in the same session; the shape of the instrumentation mattered more than the atomics did.Checked and not changed
set_sender_key_status_for_devices— 19 Ir/send at 512, not 10,298. Leave it. TheVec<String>the external profile pointed at is never built on a warm send:exclude_own_devicesfilters beforeto_string, and a warm send's SKDM list is our own companion and nothing else, so the filter yields an empty vector and the function returns early. Its whole cost is that one filtered element. The external figure is roughly 512 × 20 Ir, i.e. what the filter scan alone would cost if the list carried every member — which is a redistributing send, not a warm one. Nothing to optimize.GroupInfo::phone_jid_for_lid_user— it lives entirely inside the miss. It is called only fromresolve_group_devices_uncached(once per participant mapping in, once per resolved device mapping back), which the group memo skips on a hit. It is therefore never a target of its own while the memo hits, andrepeat_lid_group_sends_hit_both_device_memos_on_every_sendshows a LID group hits on all 30. If it ever shows up in a profile, the finding is that the memo missed, and the fix belongs there.unchanged_for— neither re-stamp nor recompute; it does not run. 0 re-stamps across every window measured, and the symbol does not appear in the send profile at all, because the generation never moves in the steady state. Its cost is consequently not on the steady-state bill, and this PR does not quote one. Test case 4 ofeach_miss_term_is_reported_as_itselfpins that an unrelated write re-stamps rather than recomputing, and that the SKDM memo still hits behind it.A stale SKDM entry is deliberately left in place when the new targets are not memoizable. It can never become valid again — the map generation only moves forward, the map
Arcis replaced wholesale on a rebuild, and the device-setWeakkeeps the old allocation alive so noptr::eqcan spuriously match. Clearing it would add a cache write on the miss path and change no outcome. What that does mean is thatnot_storedguarantees only that the next call cannot hit, not that it reportsmiss_absent; the counter documents that, and the forget-path test is the worked example (miss_map_generation, thenmiss_map).The
group_devices_memocapacity (64) is not the suspect.miss_absentis 0 across every window; it is the counter that would report an eviction, and it stays at 0.ptr::eq(memo.group_info, group_info)is not the suspect either, as the batch already suspected:miss_group_infois 0 in regime, and case 2 ofeach_miss_term_is_reported_as_itselfproves the counter can fire by publishing a freshArcinto the group cache.Every writer of
device_topology.current(), enumerated so nobody re-derives it:DeviceRegistryCache::insertand::invalidate(fused, soupdate_device_list{,s}_guarded,patch_device_add,invalidate_device_cache_guarded, the canonical-flip cleanups and the usync publication all record by construction); the one directrecord_registryon the migration-failure path inmigrate_device_registry_on_lid_discovery;LidPnCache::add_guarded(record([lid, pn])); andLidPnCache::clear(record_global, which poisons the scoped fast path).DeviceRegistryCache::promotedeliberately records nothing — it is a cache fill for an answer the DB already gave.A server-paced client does not invalidate by construction — but the reason is conditional, and worth knowing. On the receive path the only writer a steady-state message reaches is the LID↔PN learn (
cache_lid_pn_from_message, for every message whose sender carries both identifiers). It exits atcan_skip_relearnbefore any write once the pair is durably persisted and resolvable in both directions. So a warm client that has synced its mappings answers 30 inbound messages with 30 memo hits, which is whata_send_answering_an_inbound_group_message_still_hits_both_memosasserts. If persistence ever fails, or a pair is only half-resolvable, every inbound message re-records[lid, pn]and every send after it recomputes both memos — the counters would showgroup_devices.miss_topologytracking the inbound rate exactly, which is the signature to look for. The other receive-path writer,schedule_unknown_device_sync, is gated on an unknown device and deduped per user, so it does not fire in regime. That test covers the topology-visible half of a receive only; decryption, dispatch and receipts are not modelled, and a regression that made some other part of the receive path write topology would not be caught by it.Nothing was optimized, and no memo key was touched. The batch's rule was instrumentation first, and the instrumentation says there is nothing to fix on this path.
Validation
cargo fmt --allcargo clippy -p whatsapp-rust --all-targets --features bench-harness -- -D warnings— clean. The rest of the workspace matrix is left to CI (whatsapp-rust-voip-clicannot build here: itsalsa-sysbuild dependency is absent, same as bench(client): measure the group send the client crate actually pays #1283 and bench(client): address the remaining review comments on #1283 #1285).cargo test -p whatsapp-rust --lib— 1,650 passed, 0 failed, including the four new tests.tests/report_coverage.rspasses (the counters name no growable type, so they are correctly outsidememory_report()). The e2e crate is not built locally (disk); CI covers it and is green.force_skdmpath and never consult the memos at all. The window now starts wherebench_support's fixture starts, and the reason is in the test.each_miss_term_is_reported_as_itselfdrives four different invalidation causes and asserts the term, not a rate — including that a forget takes two resolves (miss_map_generationthenmiss_map, via the single-flight branch) and that an unrelated topology write re-stamps rather than recomputing.139b315on the same host in the same session.Review round
Seven bot findings, six fixed and one answered in-thread:
const fnmatches — adding a variant anywhere fails to compile.record_*still indexes byas usize, so the measured cost above is unchanged.Errrecorded no SKDM outcome, socalls()was not one per call andhit_rate()could read healthy over a shrinking denominator. AddedSkdmTargetsMemoOutcome::ResolveFailed/SkdmTargetsMemoStats::resolve_failed.hit_rate's own doc wrong in turn — a failed call skippedfilter_skdm_targetstoo, by not getting that far — so it now says memo hits over every resolver call, and why keeping failures in the denominator is the point.not_storedclaimed the next call would be amiss_absent. Corrected to what it actually guarantees (see "Checked and not changed"); the code is right, the doc was not — in two places, the counter andobservability.md, the second only caught on the follow-up round.observability.mdcounted four SKDM terms against five miss counters; it now names the four stale terms plus the entry-absent condition.555, which is not the reserved fictional NANP format.CI
Green: Clippy, Build & Test, Build & Lint (all features), Feature Matrix, Test Stable (MSRV), E2E, Rustdoc, wasm32 release, all three Miri gates, Format, Cargo Deny, Binary Size, and all four CodSpeed shards plus the performance analysis.
Semver Checks (informational)is red andcontinue-on-error: true. Confirmed pre-existing rather than assumed: the job checks-p wacore -p wacore-binary -p waproto, and this PR touches none of those crates. Every item it names is the same set #1283 and #1285 saw —mex_operationsstructs/fields/modules, theBinaryError::UnexpectedFormatBytevariant, and the removedsimdfeature — measured against the last published release rather than this PR's base. The only public surface here is additive (a new module and its types, one newClientmethod), and the one rename ispub(crate).One earlier Clippy failure was an infrastructure flake, not code:
sccache-actionaborted at setup with a TLSself-signed certificateerror, so clippy never executed. It did not recur.