Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions api/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2365,6 +2365,8 @@ See [Signal Protocol - Signed pre-key rotation (RotateKeyJob)](/advanced/signal-

Three on-`Client` surfaces answer "what does this session cost?" without any feature flag: always-on wire I/O counters via [`stats()`](#stats), an on-demand client-only memory breakdown via [`memory_report()`](#memory_report), and an on-demand unified estimate — client plus storage, transport, and HTTP — via [`resource_report()`](#resource_report). All three are dependency-free and safe to call once per client even when running many clients in one process. For CPU/custom attribution (e.g. per-session allocator tracking), see [`BotBuilder::with_task_instrument`](/api/bot#with_task_instrument) and [`BotBuilder::with_alloc_meter`](/api/bot#with_alloc_meter).

A fourth, always-on surface answers a different question — "are the group-send device-list memos actually being hit?" — via [`device_memo_stats()`](#device_memo_stats).

### stats

```rust
Expand Down Expand Up @@ -2523,6 +2525,63 @@ if let Some(alloc) = report.alloc {
Storage, transport, and HTTP reports are supplied by the trait implementations behind `Client` — see [`DeviceStore::resource_report`](/api/store#resource_report), [`Transport::resource_report`](/api/transport#resource_report), and [`HttpClient::resource_report`](/api/http-client#resource_report). `AllocSnapshot`, `StorageResourceReport`, `TransportResourceReport`, and `HttpResourceReport` are re-exported from `wacore::stats`; all four are also re-exported from the `whatsapp_rust` crate root.
</Note>

### device_memo_stats

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the diagnostics overview for this fourth surface

Adding this method leaves the Diagnostics introduction at line 2366 inaccurate: it still says there are exactly “Three on-Client surfaces” and enumerates only stats(), memory_report(), and resource_report(). Include device_memo_stats() there so the section summary remains accurate and readers using it to choose a diagnostic API can discover the new counters.

Useful? React with 👍 / 👎.


```rust
pub fn device_memo_stats(&self) -> DeviceMemoStats
```

Per-term hit/miss counts for the two device-list memos the group-send path depends on — the group-devices memo and the SKDM-targets memo — cumulative since the client was built. Always on, no feature gate. Recording is one indexed relaxed atomic add per resolver call, with one exception: a call whose resolved SKDM target set can't be memoized also bumps the separate `not_stored` counter, so that call pays two adds (see `not_stored` below). The reporting types are dropped by LTO in a binary that never calls this method.

The two memos are chained: `resolve_skdm_targets_memoized` compares the `Arc` that `resolve_group_devices_memoized` returned, so a group-memo recompute forces `skdm_targets.miss_devices` — but only when an SKDM entry already exists for the group. If none does yet (first send, or an eviction), that call reports `miss_absent` instead, regardless of what the group memo just did. Read `group_devices` first — `skdm_targets` only carries independent information once the group half is hitting.

**`GroupDevicesMemoStats` fields** (`#[non_exhaustive]`):

| Field | Type | Description |
|-------|------|-------------|
| `hits` | `u64` | Entry present, `GroupInfo` identity matched, generation unchanged |
| `restamps` | `u64` | Generation moved but every change since provably missed this group; served the same devices a hit would, at the cost of the `unchanged_for` scan |
| `miss_absent` | `u64` | No entry for the group — first send, or a capacity/TTL eviction (the memo holds 64 groups) |
| `miss_group_info` | `u64` | An entry existed but was built from a different `Arc<GroupInfo>` |
| `miss_topology` | `u64` | The device topology changed in a way that could have touched this group |
| `bypassed` | `u64` | Call didn't consult the memo at all (store-backed registry/mapping caches make its freshness contract unenforceable) |

`calls()` sums all six fields; `served_rate()` returns `(hits + restamps) / calls()` — the share resolved without a per-member registry fan-out — or `None` before the first call.

**`SkdmTargetsMemoStats` fields** (`#[non_exhaustive]`):

| Field | Type | Description |
|-------|------|-------------|
| `hits` | `u64` | Memo hit |
| `miss_absent` | `u64` | No entry — first send for this group, or an eviction |
| `miss_devices` | `u64` | The resolved device-set `Arc` differs from the memoized one — the cascade term a group-memo recompute always forces |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Qualify the forced miss_devices outcome

A group-memo recomputation does not always force miss_devices: when the SKDM memo has no entry, such as on the first send or after its eviction, the immediately preceding miss_absent definition says that lookup is classified as miss_absent, and there is no memoized device-set Arc to compare. Limit this claim to recomputations performed while an SKDM entry exists so readers do not misclassify normal cold-start misses.

Useful? React with 👍 / 👎.

| `miss_map` | `u64` | The sender-key device map was rebuilt (a warm-mark write invalidated it) |
| `miss_map_generation` | `u64` | Same map `Arc`, advanced generation — an in-place cold flip, e.g. a retry receipt's `markForgetSenderKey` |
| `miss_sender` | `u64` | A different sending identity (PN↔LID re-addressing, or a re-pair) |
| `not_stored` | `u64` | A resolved target set that couldn't be memoized, so the *next* call can't hit — but doesn't guarantee that call reports `miss_absent`: a stale entry left in place can never become valid again (the map generation only moves forward) and is reported under whichever term still fails |
| `bypassed` | `u64` | Call didn't consult the memo |
| `resolve_failed` | `u64` | The device resolution this call depends on returned `Err`, so no memo term was evaluated |

`calls()` sums every field except `not_stored`, which describes the store rather than a lookup outcome. `hit_rate()` returns `hits / calls()`. `resolve_failed` is included in that denominator on purpose — folding failed resolutions out would let a client whose group sends are failing upstream read a healthy rate. `hit_rate()` returns `None` before the first call.

`DeviceMemoStats` implements `Display` for a one-line-per-memo summary, and `since(&self, earlier: &Self) -> Self` saturating-subtracts an earlier snapshot to scope a window without resetting the counters (a reset would race a send in flight).

**Example:**

```rust
let before = client.device_memo_stats();
// ... send a batch of group messages ...
let window = client.device_memo_stats().since(&before);
println!("{window}");
if let Some(rate) = window.skdm_targets.hit_rate() {
println!("SKDM memo hit rate: {:.1}%", rate * 100.0);
}
```

<Note>
`DeviceMemoStats`, `GroupDevicesMemoStats`, and `SkdmTargetsMemoStats` are public in `whatsapp_rust::client` but, unlike `StatsSnapshot`/`MemoryReport`/`ResourceReport`, not re-exported from the crate root. Added in [#1292](https://github.com/oxidezap/whatsapp-rust/pull/1292) as a characterization tool: measured against that PR's fixtures, both memos hit on every warm send at group sizes 8–512, so the instrumentation shipped without a corresponding fix.
</Note>

## Error Types

```rust
Expand Down