Skip to content

perf(send): stamp one message from one clock read - #1106

Merged
jlucaso1 merged 3 commits into
mainfrom
perf/send-one-instant
Jul 25, 2026
Merged

perf(send): stamp one message from one clock read#1106
jlucaso1 merged 3 commits into
mainfrom
perf/send-one-instant

Conversation

@jlucaso1

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #1103, applied to the send path. Sending one direct message read the wall clock four times on the client's own thread for what is a single logical instant: the message id, the biz node, the privacy-token decision and the outbound message secret. Sample it once as SendInstant and carry it down. Besides the reads, this fixes an incoherence: four reads microseconds apart can straddle a second boundary and leave one message described by different seconds in its id, its stanza and its store row.

Audit

Measured with the clock_reads counter #1103 added, attributing every read by stack over one steady-state send (registry, LID mapping and Signal sessions warm, one frame written):

call site before after
RequestUtils::generate_message_id 1 0
biz node in send_message_with_options_inner 1 0
should_send_new_tc_token_with 1 0
persist_outbound_msg_secret 1 0
SendInstant::now 0 1
wall total 4 1

Not visible in that harness but on the same instant: is_tc_token_expired_with runs whenever a stored token exists, so a real send against a known contact read five times, not four. Both privacy-token decisions now take the shared instant.

The code already knew these were one instant. persist_outbound_msg_secret carried a comment saying it wants "the parent event time" and re-read the clock only because it had no way to receive one.

Changes

  • SendInstant, sampled at the top of send_message_with_options_inner and carried through SendPipelineOptions into the DM branch, the privacy-token decision and the outbound secret. send_message_impl samples its own when a caller did not, so the other entry points keep exactly one read too.
  • generate_message_id_at, should_send_new_tc_token_with_at, is_tc_token_expired_with_at: _at variants that take the instant, matching should_send_new_tc_token_at and the is_dead_socket_at shape from perf(stats): stop dating every wire frame for a field nothing reads #1103. The clock-reading versions stay, so nothing becomes a required parameter of the public API.
  • src/send/mod.rs gains the cost-model line the module lacked, which is how it accumulated four reads without anyone noticing.
  • The stats cost model now records that one direct message arrives as roughly four transport events, so "one read per event" is four per message. That corrects the assumption perf(stats): stop dating every wire frame for a field nothing reads #1103 reasoned from, without changing its conclusion.

Left alone, and why

Each of these is recorded next to the code so the next reader does not redo the analysis.

  • Device-registry lookups (2 monotonic reads/send). They could share one instant: the TTL is an hour, so microseconds cannot change the outcome. Sharing it needs a clock-taking method on PortableCache and TypedCache, both public, and the parameter would be meaningless for store-backed caches whose TTL lives in the store. One read per send did not justify that.
  • Chat-lane latency guard (2 monotonic reads/message). Sampling it, or only measuring when the lane has a backlog, both stop reporting the single pathological message the guard exists to catch. A controlled-clock test of it is not reachable either: the monotonic provider is the same process-wide OnceLock, which is what pushed perf(stats): stop dating every wire frame for a field nothing reads #1103 to count at the boundary in the first place.
  • SessionStats per-frame and per-event bookkeeping. perf(stats): stop dating every wire frame for a field nothing reads #1103 rejected sampling last_data_received_ms because two decisions measure elapsed time from it, and rejected deriving the watchdog from tick-observed counters because it pushes worst-case detection from ~50s to ~80s. The new data (four events per message, not one) changes the size of the group, not either argument.

Guarantees

  • a_send_reads_the_clock_once asserts exactly one wall read across a send, which is the proof that every stamp shares an instant, and checks the outbound secret really landed inside the measured window.
  • dm_send_stays_within_its_clock_budget drops its budget from 4 wall reads to 1 and fails if it grows.
  • wire_timestamp_keeps_real_time still proves a timestamp reaching the server carries the real second.
  • supplied_instant_decides_the_same_bucket_boundary pins both privacy-token decisions at the exact bucket boundary against a supplied instant.
  • The PortableCache TTL/TTI boundary test and the dead-socket tests from perf(stats): stop dating every wire frame for a field nothing reads #1103 pass unchanged.

No embedder has to register a 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.

Sending a direct message read the wall clock four times on the client's own
thread: once for the id, once for the biz node, once for the privacy-token
decision and once for the outbound message secret, plus a second privacy-token
read whenever a token already exists. They all describe the same instant, and
the code already knew it: the message-secret site commented that it wants "the
parent event time" and re-read the clock only because it had no way to receive
one.

Sample it once as `SendInstant` where the operation starts and carry it down.
Besides the reads, this makes the stamps coherent: four reads microseconds
apart can straddle a second boundary and leave one message described by
different seconds in the id, the stanza and the store.

The instant reaches its consumers through `_at` variants of the existing
helpers, matching the shape already used by `should_send_new_tc_token_at` and
by `is_dead_socket_at` from #1103. Nothing is added to the public API as a
required parameter.

Left alone, with the reasoning recorded where a reader will find it: the two
monotonic reads of the device-registry lookups (sharing them needs a
clock-taking method on public cache types for one read), the chat-lane latency
guard (sampling or gating on backlog would stop reporting the single slow
message it exists to catch), and the per-frame session-stats bookkeeping.
@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

📝 Walkthrough

Summary by CodeRabbit

  • Improvements
    • Send operations now use a consistent, single sampled timestamp across message IDs, outbound message secrets, interactive content, and privacy-token decisions.
    • Time-bound privacy-token validation and renewal are more consistent at boundary conditions.
    • Message persistence and subsequent processing remain better synchronized.
  • Documentation
    • Clarified timing/backlog-monitoring behavior in per-chat message processing.
  • Tests
    • Updated and added coverage to verify correct secret persistence timing and a reduced wall-clock read budget during sends.

Walkthrough

The send pipeline now samples one wall-clock instant and reuses it for message IDs, business-node timestamps, tc-token decisions, and outbound-secret persistence. Timestamp-aware helpers, updated callers, tests, and documentation support the single-instant model.

Changes

Single send instant

Layer / File(s) Summary
Timestamp-aware request and token APIs
wacore/src/request.rs, src/request.rs, wacore/src/iq/tctoken.rs
Message-ID and tc-token helpers accept caller-supplied timestamps while preserving current-time wrappers.
Single-instant send pipeline
src/send/mod.rs
SendInstant is propagated through send branches, request generation, business-node inference, secret persistence, and clock-budget tests.
Tc-token integration
src/send/tctoken_lifecycle.rs
Tc-token issuance and expiration checks use the propagated send timestamp.
Supporting callers and timing documentation
src/features/comments.rs, src/message/tests.rs, src/handlers/message.rs, wacore/src/stats.rs
Additional persistence callers provide SendInstant, and timing-related comments and documentation are updated.

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

Possibly related PRs

Suggested labels: api-design, performance

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

Sequence Diagram(s)

sequenceDiagram
  participant Sender
  participant SendPipeline
  participant RequestUtils
  participant TcTokenLifecycle
  participant SecretStore

  Sender->>SendPipeline: start send
  SendPipeline->>SendPipeline: sample SendInstant
  SendPipeline->>RequestUtils: generate message ID at sampled seconds
  SendPipeline->>TcTokenLifecycle: evaluate tc-token state at sampled seconds
  SendPipeline->>SecretStore: persist outbound secret at sampled seconds
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly captures the main change: reducing send-path clock reads by stamping a message from one sampled instant.
Description check ✅ Passed The description matches the changeset and accurately explains the one-read send-path timestamping update.
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/send-one-instant

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

Copy link
Copy Markdown

Greptile Summary

The PR consolidates send-path timestamps around one shared wall-clock sample.

  • Introduces SendInstant and propagates it through message-ID generation, business metadata, privacy-token decisions, and outbound-secret persistence.
  • Adds caller-supplied timestamp variants for message-ID and trusted-contact-token helpers while preserving existing clock-reading APIs.
  • Adds clock-budget and boundary tests and documents retained clock-read costs.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/send/mod.rs Introduces and propagates the shared send instant through the main send pipeline and outbound-secret persistence.
src/send/tctoken_lifecycle.rs Uses the send instant consistently for trusted-contact-token issuance and expiration decisions.
wacore/src/iq/tctoken.rs Adds timestamp-injected token helpers while retaining behavior-compatible clock-reading wrappers.
wacore/src/request.rs Adds timestamp-injected message-ID generation and preserves the existing public convenience method.
src/features/comments.rs Adapts comment-secret persistence to the new explicit timestamp parameter.
src/message/tests.rs Updates message-secret tests for the explicit send-instant contract.
src/request.rs Adds the client-level timestamp-injected message-ID helper used by the send pipeline.
src/handlers/message.rs Documents why chat-lane latency measurement retains separate monotonic clock reads.
wacore/src/stats.rs Clarifies the per-message transport-event clock-read cost model.

Sequence Diagram

sequenceDiagram
  participant Caller
  participant Send as Send pipeline
  participant Metadata
  participant Token as Privacy-token logic
  participant Store as Secret store
  Caller->>Send: send message
  Send->>Send: SendInstant::now()
  Send->>Metadata: generate ID and biz node at instant
  Send->>Token: evaluate token state at instant
  Send->>Store: persist outbound secret at instant
Loading

Reviews (2): Last reviewed commit: "fix(send): stamp the fallback id from th..." | Re-trigger Greptile

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 25, 2026

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

1731-1735: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Propagate the original SendInstant through every send continuation.

The fallback ID path, async tc-token issuance, and comment-secret persistence resample the wall clock. This breaks the one-read/coherent-stamp contract, especially around second and token-bucket boundaries.

  • src/send/mod.rs#L1731-L1735: generate fallback IDs with self.generate_message_id_at(sent_at.unix_secs_u64()).
  • src/send/mod.rs#L1873-L1882: pass sent_at into an _at issuance path; use it for the privacy IQ timestamp and sender-timestamp write.
  • src/features/comments.rs#L111-L124: retain the instant sampled for the comment send and reuse it when persisting comment_secret, rather than calling SendInstant::now() afterward.
🤖 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 1731 - 1735, Propagate the original SendInstant
through all send continuations: in src/send/mod.rs lines 1731-1735, use
sent_at.unix_secs_u64() with generate_message_id_at for fallback IDs; in
src/send/mod.rs lines 1873-1882, pass sent_at to the _at token-issuance path and
reuse it for the privacy IQ timestamp and sender-timestamp write; in
src/features/comments.rs lines 111-124, retain the instant sampled for the
comment send and reuse it when persisting comment_secret instead of calling
SendInstant::now().
🤖 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 1731-1735: Propagate the original SendInstant through all send
continuations: in src/send/mod.rs lines 1731-1735, use sent_at.unix_secs_u64()
with generate_message_id_at for fallback IDs; in src/send/mod.rs lines
1873-1882, pass sent_at to the _at token-issuance path and reuse it for the
privacy IQ timestamp and sender-timestamp write; in src/features/comments.rs
lines 111-124, retain the instant sampled for the comment send and reuse it when
persisting comment_secret instead of calling SendInstant::now().

ℹ️ Review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 027e0682-1ce5-4471-99e0-56444d326105

📥 Commits

Reviewing files that changed from the base of the PR and between 7b26749 and c5e8122.

📒 Files selected for processing (9)
  • src/features/comments.rs
  • src/handlers/message.rs
  • src/message/tests.rs
  • src/request.rs
  • src/send/mod.rs
  • src/send/tctoken_lifecycle.rs
  • wacore/src/iq/tctoken.rs
  • wacore/src/request.rs
  • wacore/src/stats.rs

The module doc pointed at `SendInstant`, which is crate-private, so rustdoc
rejected the link.
@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 +192 B (+0.00%) 🔺
bin .text 7.98 MiB 7.98 MiB +320 B (+0.00%) 🔺
bin allocated (text+data+bss) 9.93 MiB 9.93 MiB +16 B (+0.00%) 🔺
llvm-lines wacore 490,734 490,740 +6 (+0.00%) 🔺
llvm-lines wacore copies 16,320 16,323 +3 (+0.02%) 🔺
llvm-lines whatsapp-rust lib 703,844 703,970 +126 (+0.02%) 🔺
llvm-lines whatsapp-rust lib copies 22,166 22,173 +7 (+0.03%) 🔺
deps crates (Cargo.lock) 471 471 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.77 MiB 1.77 MiB +745 B (+0.04%) 🔺
.text wacore 648.16 KiB 647.75 KiB -423 B (-0.06%) 🔽
.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 +28 B (+0.00%) 🔺
.text other deps 1.89 MiB 1.89 MiB 0

Baseline: 7b26749d3 (latest main run) · Head: 65fa4e2f5 · Graphs

`send_message_impl` still read the clock for its own fallback id, so the entry
points that do not come through `send_message_with_options_inner` kept two
reads instead of one.

Also drop the module doc's intra-doc link to `SendInstant`: the type is
crate-private, which rustdoc rejects.
@greptile-apps
greptile-apps Bot dismissed their stale review July 25, 2026 16:47

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

@jlucaso1

Copy link
Copy Markdown
Collaborator Author

Took the first of the three, skipped the other two.

send_message_impl really was resampling for its fallback id, so the entry points that do not go through send_message_with_options_inner kept two reads instead of one. Fixed.

Not taking the tc-token issuance one: issue_tc_token_after_send is fire-and-forget on its own task, after the send returned. Its t= goes on the wire and the server echoes it, and the stored sender timestamp drives the bucket rate limit. Stamping that IQ with the send instant would make a wire timestamp older than the event it describes, which is reading it worse rather than reading it once. The shared instant is meant to stop where the operation does.

Not taking the comments one either: the comment secret is persisted after send_message returns, and SendResult does not carry the instant, so reusing it means widening a public non_exhaustive type or duplicating a send entry point for one read off the hot path. The skew it would remove lands in message_ts, which feeds retention horizons measured in days.

Also fixed the rustdoc failure in the same push: the module doc linked SendInstant, which is crate-private.

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

2541-2561: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve the sampled instant for external persistence callers.

src/features/comments.rs:54-127 calls send_message(...), then passes SendInstant::now() here. That performs a second wall-clock read and can give the comment secret a later message_ts/expiry than the message’s actual send instant, breaking the one-send/one-instant contract. Return the sampled instant with the send result, or expose an API that lets the caller reuse it.

🤖 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 2541 - 2561, Update the send flow and
persist_outbound_msg_secret integration so external callers reuse the instant
sampled during send_message rather than calling SendInstant::now() afterward.
Return the sampled SendInstant with the send result, or expose an equivalent
API, and ensure features/comments.rs passes that same instant to persistence
while preserving the one-send/one-instant contract.
🤖 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 2541-2561: Update the send flow and persist_outbound_msg_secret
integration so external callers reuse the instant sampled during send_message
rather than calling SendInstant::now() afterward. Return the sampled SendInstant
with the send result, or expose an equivalent API, and ensure
features/comments.rs passes that same instant to persistence while preserving
the one-send/one-instant contract.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fd2c1210-da7c-4da8-a251-6a1c388e06fa

📥 Commits

Reviewing files that changed from the base of the PR and between c5e8122 and 16db599.

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

@jlucaso1
jlucaso1 merged commit 686e656 into main Jul 25, 2026
21 of 22 checks passed
@jlucaso1
jlucaso1 deleted the perf/send-one-instant branch July 25, 2026 17:00
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