Skip to content

perf(stats): stop dating every wire frame for a field nothing reads - #1103

Merged
jlucaso1 merged 3 commits into
mainfrom
perf/clock-reads-hot-path
Jul 25, 2026
Merged

perf(stats): stop dating every wire frame for a field nothing reads#1103
jlucaso1 merged 3 commits into
mainfrom
perf/clock-reads-hot-path

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

A consumer reported the core asking for the time 16 times per direct message in a ping-pong benchmark, which on a platform without a cheap clock is a boundary crossing per read. This batch started as an audit of that attribution and ends with one real cut plus a decision to leave the rest exact. The cut: SessionStats::record_frame_sent read the wall clock on every frame written, and only one of the two fields it fed actually needed an instant.

Audit

I could not run the reporter's embedder, so I instrumented the core instead. wacore::time now counts every read that leaves the module, behind the existing test-util feature. Counting at the boundary rather than inside a provider sidesteps the OnceLock: the process's first now_millis() fills TIME_PROVIDER with the default, so a suite sharing one process cannot install an instrumented provider per test. The counters are per thread, so a measurement stays exact while the rest of the suite runs in parallel.

Measured on a warm client (registry, LID mapping and Signal sessions seeded) sending one direct message, counting the core's own thread:

before after
wall reads 5 4
monotonic reads 2 2

Per-frame and per-event bookkeeping, measured directly on SessionStats:

before after
frame sent, anchor already armed 1 0
frame sent, arming the anchor 1 1
received transport event 1 1

How much this is worth depends on the traffic shape, and it is worth saying plainly. The anchor is cancelled by every receive, so in a strictly alternating request/response pattern the first send after each receive re-arms and still pays: only the extra frames of the same burst come out free, and the saving is small by construction. It scales with how many frames a send burst writes before the next receive, so offline sync, group fanout and bulk sends are where it actually pays.

Two corrections to the original attribution:

  • last_data_received_ms has two internal readers, not one. Besides is_dead_socket, the keepalive loop reads it at src/keepalive.rs:165 to decide whether to skip the ping. Both measure elapsed time, so the field cannot be sampled: the only refresh trigger the core has is the keepalive tick itself, which is 15-30 s apart and coarser than the 15 s gate it would feed. Deriving the watchdog from counters observed at tick boundaries was the other option, and it pushes worst-case detection from ~50 s to ~80 s. Rejected, and the reasoning now lives next to the field.
  • last_data_sent_ms had zero internal readers. Nothing in the core ever called the getter. That is the read this PR removes.

The cache finding held up, with a caveat: PortableCache::get did sample the monotonic clock before the lookup, so a miss paid for a timestamp nothing compared. But in the steady state that dominates a ping-pong the registry lookups are hits, so moving the read after the lookup measures the same 2 reads per send here. It is still the correct shape and it is now pinned by a test.

Changes

  • SessionStats reads the clock only when the dead-socket anchor arms, not per frame. Sends that continue an already-armed burst cost nothing.
  • Breaking: StatsSnapshot::last_data_sent_ms and SessionStats::last_data_sent_ms() removed. Migration: frames_sent answers "is it still sending?", first_send_since_recv_ms() dates the send the watchdog anchors on. An embedder that needs "when did I last write?" should stamp it at its own send call site rather than make the wire path pay for it.
  • PortableCache::get/remove read the monotonic clock after the lookup, so a miss is free.
  • is_dead_socket_at / ms_since_at take the instant explicitly. The keepalive tick's dead-socket branch now evaluates both against one read, and the watchdog boundary becomes testable without depending on how long a test took to run.
  • wacore::time::clock_reads (test-util only) counts reads per thread.
  • The stats "Cost model" section now states the per-frame and per-event read counts, so the next PR cannot multiply them without noticing.
  • create_iq_test_client wires the noise socket to the client's stats like the real one does, so per-frame bookkeeping is part of what tests observe.

Guarantees

Written as tests, not prose:

  • wire_bookkeeping_reads_the_clock_only_where_a_value_is_used pins 0 reads for sends under an armed anchor, 1 for the arming send, 1 per received event, 2 for a multi-frame batch.
  • dm_send_stays_within_its_clock_budget asserts the per-send budget and fails if it grows.
  • received_stanza_handling_reads_no_clock pins that handling a stanza asks for no time.
  • dead_socket_boundary_is_exact and continued_sends_do_not_hide_a_silent_socket prove detection against a supplied instant, including the exact deadline. The existing dead_socket_anchor_holds_across_continued_sends and stale_pre_receive_anchor_self_heals_on_next_send pass unchanged.
  • expiry_boundary_is_exact_under_a_controlled_clock covers the entry that expires exactly at the TTL and TTI deadlines; a_miss_does_not_read_the_clock pins the lazy read.
  • wire_timestamp_keeps_real_time proves a timestamp that reaches the server still carries the real second.

No embedder has to register a new provider, change a provider signature, or keep time state of its own.

Validation

cargo fmt --all --check
cargo test -p wacore --lib
cargo test -p whatsapp-rust --lib
cargo clippy --workspace --all-targets -- -D warnings

Full matrix left to CI.

`SessionStats::record_frame_sent` read the wall clock on every frame written,
which is the client's hottest path and, on wasm32 or embedded targets, a call
out of the module. Only one of the two fields it fed needed an instant: the
dead-socket anchor, which arms once per receive-to-send transition. The other,
`last_data_sent_ms`, had no reader anywhere in the core, because the watchdog
deliberately anchors on the first unanswered send and never on the most recent
one.

Drop the field and read the clock only when the anchor actually arms. A send
that continues an already-armed burst now asks for no time at all. The
receive-side stamp stays exact: two decisions measure elapsed time from it,
the 15 s idle-ping gate and the 20 s dead-socket check, and the only refresh
trigger the core could sample it against is the keepalive tick, which is
coarser than the gate it would feed.

The cache reads move for the same reason: `PortableCache::get` and `remove`
sampled the monotonic clock before the lookup, so every miss paid for a
timestamp there was nothing to compare it against.

BREAKING: `StatsSnapshot::last_data_sent_ms` and
`SessionStats::last_data_sent_ms()` are gone. `frames_sent` answers "is it
still sending?" and `first_send_since_recv_ms()` dates the send the watchdog
cares about; an embedder that needs "when did I last write?" should stamp it
at its own send call site.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4edbaa79-6fdc-4171-89ac-1bd91ad2808b

📥 Commits

Reviewing files that changed from the base of the PR and between b81bb05 and 616260b.

📒 Files selected for processing (2)
  • agent_docs/observability.md
  • wacore/src/stats.rs

📝 Walkthrough

Summary by CodeRabbit

  • Performance

    • Reduced unnecessary clock reads during message sending and cache misses, improving efficiency on common paths.
    • Preserved accurate cache expiration behavior at timeout boundaries.
  • Reliability

    • Improved dead-connection detection by using more precise activity timing.
    • Ongoing outgoing traffic no longer masks an otherwise inactive connection.
  • Observability

    • Updated connection activity statistics to reflect the current send/receive tracking model.

Walkthrough

Session statistics now use a conditional first-send watchdog anchor instead of per-frame last-send timestamps. Clock instrumentation, deterministic keepalive timing, cache fast paths, and send-path tests validate the revised behavior.

Changes

Clock-aware activity metering

Layer / File(s) Summary
Session stats clock model
wacore/src/time.rs, wacore/src/stats.rs, agent_docs/observability.md
Adds test-only clock counters, removes last_data_sent_ms, and conditionally maintains first_send_since_recv_ms.
Deterministic keepalive timing
wacore/src/protocol/keepalive.rs, src/keepalive.rs
Adds timestamp-injected predicates and uses one captured timestamp for dead-socket checks and warnings.
Conditional cache clock reads
src/portable_cache.rs
Defers monotonic reads until cache entries are found and tests expiry boundaries plus hit/miss counts.
Send-path clock budgets
src/send/mod.rs, src/client/tests.rs, src/test_utils.rs, src/socket/noise_socket.rs
Adds clock-budget, receipt-processing, timestamp, and transport-statistics tests, with test sockets wired to session stats.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant KeepaliveLoop
  participant SessionStats
  participant KeepaliveProtocol
  KeepaliveLoop->>SessionStats: Read activity timestamps
  KeepaliveLoop->>KeepaliveProtocol: Evaluate is_dead_socket_at with captured now
  KeepaliveProtocol-->>KeepaliveLoop: Return dead-socket status
  KeepaliveLoop->>KeepaliveProtocol: Compute warning duration with ms_since_at
Loading

Possibly related PRs

Suggested labels: performance, breaking-change

Suggested reviewers: cubic-dev-ai

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: removing unnecessary per-frame wire clock reads and dropping the unused last-sent field.
Description check ✅ Passed The description is directly related to the patch and accurately describes the clock-read audit, stats changes, and added tests.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/clock-reads-hot-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.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 9.93 MiB 9.93 MiB +7.00 KiB (+0.07%) 🔺
bin .text 7.97 MiB 7.98 MiB +7.00 KiB (+0.09%) 🔺
bin allocated (text+data+bss) 9.92 MiB 9.93 MiB +8.02 KiB (+0.08%) 🔺
llvm-lines wacore 490,764 490,734 -30 (-0.01%) 🔽
llvm-lines wacore copies 16,319 16,320 +1 (+0.01%) 🔺
llvm-lines whatsapp-rust lib 703,709 703,844 +135 (+0.02%) 🔺
llvm-lines whatsapp-rust lib copies 22,162 22,166 +4 (+0.02%) 🔺
deps crates (Cargo.lock) 471 471 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.77 MiB 1.77 MiB +6.88 KiB (+0.38%) 🔺
.text wacore 647.86 KiB 648.16 KiB +307 B (+0.05%) 🔺
.text wacore_binary 89.42 KiB 89.42 KiB 0
.text wacore_libsignal 161.89 KiB 161.89 KiB 0
.text wacore_appstate 22.36 KiB 22.36 KiB 0
.text wacore_noise 21.60 KiB 21.60 KiB 0
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 514.77 KiB 514.77 KiB 0
.text whatsapp_rust_tokio_transport 39.91 KiB 39.91 KiB 0
.text whatsapp_rust_ureq_http_client 10.40 KiB 10.40 KiB 0
.text std 1.07 MiB 1.07 MiB +55 B (+0.00%) 🔺
.text other deps 1.89 MiB 1.89 MiB -231 B (-0.01%) 🔽
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust 1.77 MiB 1.77 MiB +6.88 KiB (+0.38%)

Baseline: 4ae76fcb6 (latest main run) · Head: 868e5bc07 · Graphs

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

🤖 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/send/mod.rs`:
- Line 5604: Update the OWN_PN constant to use the repository’s fictional NANP
format, specifically a number in the required 555-01xx layout such as
12025550101.

In `@wacore/src/protocol/keepalive.rs`:
- Around line 26-30: Complete the helper documentation near ms_since by adding
the missing verb so the sentence clearly states what the dead-socket keepalive
tick evaluates against a caller-supplied now. Preserve the existing explanation
that ms_since and is_dead_socket_at use the same instant and that tests can
provide it directly.

In `@wacore/src/stats.rs`:
- Around line 121-125: Update the anchor arming logic in the surrounding stats
method to use a single atomic compare-and-update operation, so concurrent first
sends preserve the earliest successful timestamp instead of overwriting it;
retain the existing anchor == 0 or anchor <= last_recv stale-anchor recovery
behavior, and add a concurrent regression test covering competing first sends.
- Around line 12-16: The receive clock-read documentation is inaccurate for
multi-frame batches. Update wacore/src/stats.rs lines 12-16 to state that
single-frame receives use one wall-clock read, while multi-frame batches use
two—at arrival and completion; update agent_docs/observability.md lines 35-41 to
express the same observability contract. No code behavior changes are required.
🪄 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: 6ab098c9-0596-4b49-8432-f4b36340c79c

📥 Commits

Reviewing files that changed from the base of the PR and between 4ae76fc and d002493.

📒 Files selected for processing (10)
  • agent_docs/observability.md
  • src/client/tests.rs
  • src/keepalive.rs
  • src/portable_cache.rs
  • src/send/mod.rs
  • src/socket/noise_socket.rs
  • src/test_utils.rs
  • wacore/src/protocol/keepalive.rs
  • wacore/src/stats.rs
  • wacore/src/time.rs

Comment thread src/send/mod.rs
use std::sync::Arc;
use wacore::time::clock_reads;

const OWN_PN: &str = "15551234001";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the repository’s fictional NANP format.

15551234001 uses 555 as its NPA and does not use the required 555-01xx fictional layout. Use a value such as 12025550101.

🤖 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` at line 5604, Update the OWN_PN constant to use the
repository’s fictional NANP format, specifically a number in the required
555-01xx layout such as 12025550101.

Source: Learnings

Comment thread wacore/src/protocol/keepalive.rs Outdated
Comment thread wacore/src/stats.rs Outdated
Comment thread wacore/src/stats.rs Outdated
…anchor

The load-check-store let two senders that both observe an unarmed anchor
overwrite each other, so the later timestamp won and pushed the dead-socket
deadline out. Re-check under a CAS: the plain load still gates the clock read,
so a send under an armed anchor stays free, and the arm itself now preserves
the first one.

Also state the multi-frame receive cost, which re-stamps on batch completion
and therefore spends a second read.
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

Took three of the four:

  • Anchor arming is now a CAS that re-checks the same condition, so a race between two first sends keeps the earlier deadline. The plain load still gates the clock read, so the free path is unchanged.
  • Cost model and the observability doc now say the multi-frame batch spends a second read on completion.
  • Reworded the two _at helper docs.

Skipped the phone number: 15551234001 is copied from the existing test at src/send/mod.rs:2745, and the repo convention is plain 555XXXXXXX (5551234567 alone shows up 135 times in src/). Changing just this one would make it the odd one out.

On the CAS, worth noting for whoever reads this later: record_frame_sent is only reached from the noise sender task, which processes send jobs serially, so the race was not reachable on the main session socket. Took it anyway since it costs nothing.

@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

Caution

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

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

856-856: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the reset test prove that receive activity was recorded.

record_recv_batch(20, 1) does not stamp last_data_received_ms; that happens in mark_recv_activity(). Without seeding receive activity, the post-reset zero assertion passes even if reset stops clearing that field.

Add mark_recv_activity() before the send/reset sequence and assert the timestamp is non-zero before teardown.

🤖 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/stats.rs` at line 856, Update the reset test around
first_send_since_recv_ms to call mark_recv_activity() before the send/reset
sequence, then assert the recorded receive timestamp is non-zero before teardown
so the reset assertion verifies that reset clears it.
🤖 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 `@agent_docs/observability.md`:
- Around line 35-40: Update the documentation around record_frame_sent and
first_send_since_recv_ms to clarify that the timestamp is loaded on every frame,
while now_ms() is called only when a send arms or re-arms the anchor. Preserve
the existing explanation that there is no last-send timestamp.

---

Outside diff comments:
In `@wacore/src/stats.rs`:
- Line 856: Update the reset test around first_send_since_recv_ms to call
mark_recv_activity() before the send/reset sequence, then assert the recorded
receive timestamp is non-zero before teardown so the reset assertion verifies
that reset clears it.
🪄 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: 01e91483-f0c4-4646-996c-32693921ebcd

📥 Commits

Reviewing files that changed from the base of the PR and between d002493 and b81bb05.

📒 Files selected for processing (3)
  • agent_docs/observability.md
  • wacore/src/protocol/keepalive.rs
  • wacore/src/stats.rs

Comment thread agent_docs/observability.md
@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

This PR removes the last_data_sent_ms field and its per-frame clock read from SessionStats, replacing the unconditional store with a CAS-gated read that fires only when the dead-socket anchor first arms. It also moves the monotonic clock read in PortableCache::get/remove to after the key lookup so cache misses pay nothing, adds _at variants to the keepalive predicates so the tick evaluates both checks against one timestamp, and introduces per-thread clock_reads counters (behind test-util) that let tests assert exact budgets instead of estimating them.

  • SessionStats drops last_data_sent_ms (no internal readers existed) and rewrites record_frame_sent with a CAS: now_ms() is only called when the outer load sees an unarmed/stale anchor; subsequent frames in the same burst pay only two relaxed loads.
  • PortableCache moves entry_time() after the ?-guarded map lookup in both get (no-TTI and TTI paths) and remove, making every miss clock-free.
  • Keepalive tick captures one now_ms() and passes it to both is_dead_socket_at and ms_since_at, eliminating a second read on the dead-socket branch.

Confidence Score: 5/5

Safe to merge. The changes are narrowly scoped to clock-read paths that are fully covered by new boundary tests; the breaking removal of last_data_sent_ms is internally clean since nothing in the core read the field.

All three core changes — the CAS-gated anchor arming, the lazy cache clock read, and the single-read keepalive tick — have self-consistent logic and are pinned by tests that assert exact clock-read counts at known boundaries. The self-healing stale-anchor property is preserved by the CAS condition and covered by the existing stale_pre_receive_anchor_self_heals_on_next_send test, which passes unchanged.

Files Needing Attention: No files require special attention. The most complex change is in wacore/src/stats.rs (the CAS rewrite of record_frame_sent), but it is well-tested by both the existing dead_socket_anchor_holds_across_continued_sends test and the new wire_bookkeeping_reads_the_clock_only_where_a_value_is_used test.

Important Files Changed

Filename Overview
wacore/src/stats.rs Removes last_data_sent_ms field and replaces unconditional store with a CAS-gated clock read in record_frame_sent. Logic is sound: the outer load short-circuits the clock read on already-armed anchors, and the CAS preserves onOrBefore semantics. Breaking API change (StatsSnapshot::last_data_sent_ms removed) is well-justified by absence of internal readers.
wacore/src/time.rs Adds clock_reads module under #[cfg(feature = "test-util")]: per-thread saturating counters for wall and monotonic reads, bumped inside now_millis() and now_nanos() respectively. Design correctly avoids the OnceLock provider-installation race by counting at the abstraction boundary rather than inside a replaceable provider.
wacore/src/protocol/keepalive.rs Adds is_dead_socket_at, ms_since_at, and now_ms() taking an explicit instant, delegated to by the original functions. The keepalive tick now reads the clock once and passes it to both predicates. New boundary tests pin exact detection semantics.
src/portable_cache.rs Moves entry_time() calls after the key lookup in get (both no-TTI and TTI paths) and remove, so misses pay zero monotonic reads. The same now snapshot is reused for the write-lock double-check on expiry, which is safe: a freshly re-inserted entry has inserted_at > stale_now, so is_expired returns false and the entry is not wrongly evicted.
src/keepalive.rs Dead-socket branch now captures one now_ms() and passes it to both is_dead_socket_at and ms_since_at, eliminating the second clock read the old is_dead_socket / ms_since pair each made independently.
src/client/tests.rs Adds wire_bookkeeping_reads_the_clock_only_where_a_value_is_used (pins CAS arming read counts) and received_stanza_handling_reads_no_clock (pins zero reads for receipt handling). Both use clock_reads::since to assert exact budgets.
src/send/mod.rs Adds dm_send_stays_within_its_clock_budget (asserts wall <= 4, monotonic <= 2 per steady-state DM send) and wire_timestamp_keeps_real_time (verifies the privacy-token IQ still carries a real second despite clock-read reduction). Budget expressed as upper bounds to catch regressions without being brittle to minor changes.
src/test_utils.rs create_iq_test_client now wires NoiseSocket to the client's stats via with_stats, so per-frame bookkeeping is observed by budget tests that go through this helper.
agent_docs/observability.md Updates the cost-model description to reflect the removed last_data_sent_ms field, explain the new first_send_since_recv_ms anchor semantics, and document the deliberate absence of a per-frame send timestamp.
src/socket/noise_socket.rs Test updated from snap.last_data_sent_ms to stats.first_send_since_recv_ms() following the removal of the sent timestamp from StatsSnapshot.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[record_frame_sent] --> B[fetch_add bytes and frames]
    B --> C[load last_recv and anchor]
    C --> D{anchor == 0 OR anchor <= last_recv}
    D -- No: anchor is live --> E[Return, zero clock reads]
    D -- Yes: unarmed or stale --> F["now = now_ms() - one wall read"]
    F --> G["CAS: current == 0 OR current <= last_recv"]
    G -- CAS won --> H[first_send_since_recv_ms = now]
    G -- CAS lost --> I[Earlier anchor preserved, now discarded]

    subgraph keepalive_tick["Keepalive tick - dead socket branch"]
        K[Read first_send and last_recv from stats]
        K --> L["now = now_ms() - one wall read"]
        L --> M[is_dead_socket_at first_send, last_recv, now]
        M -- dead --> N[reconnect_immediately]
        M -- alive --> O["ms_since_at first_send, now - log elapsed, no extra read"]
    end
Loading

Reviews (3): Last reviewed commit: "docs(stats): say what the anchor CAS act..." | Re-trigger Greptile

Comment thread wacore/src/stats.rs
The comment claimed racing senders keep the earlier deadline. That only holds
once an anchor is set; two senders that both observe it unarmed resolve by
whichever CAS lands first, which may be the one carrying the later instant.
State the real guarantee, and note that the serial sender task makes the case
unreachable anyway.

Also correct the observability doc: every frame loads the anchor, only the
send that arms it spends a clock read.
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

Both fair, and the CAS one is a real correction on my part.

Greptile is right that the comment overstated it: the CAS guarantees that once an anchor is set a later send cannot overwrite it, but two senders that both observe it unarmed resolve by whichever CAS lands first, which may well be the one holding the later instant. My commit message from the previous push said "keep the earlier deadline", which is wrong for the same reason. Comment now states the actual guarantee and notes the serial sender task makes the case unreachable in the first place.

Also reworded the observability doc: the anchor is loaded on every frame, only the arming send spends the clock read.

@jlucaso1
jlucaso1 merged commit 7b26749 into main Jul 25, 2026
17 of 19 checks passed
@jlucaso1
jlucaso1 deleted the perf/clock-reads-hot-path branch July 25, 2026 04:04
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