Skip to content

perf(history-sync)!: store the compressed payload and expose a streaming reader - #853

Merged
jlucaso1 merged 9 commits into
mainfrom
perf/history-sync-compressed-streaming
Jun 11, 2026
Merged

perf(history-sync)!: store the compressed payload and expose a streaming reader#853
jlucaso1 merged 9 commits into
mainfrom
perf/history-sync-compressed-streaming

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Why

Event::HistorySync carried the fully decompressed blob (a typical InitialBootstrap chunk is 5-20 MB inflated, ~10x the compressed size), so any consumer with a HistorySync handler paid O(decompressed) retained memory per queued event, and incremental consumption required hand-rolling a protobuf wire walk. Internally we also kept two copies of that walk (a streaming one and a full-buffer one) guarded by a parity test.

What changed

The event stores compressed bytes. LazyHistorySync now holds the original zlib payload as one immutable Bytes plus the exact decompressed_size counted by the extraction pass. Holding or queueing the event costs O(compressed). The Mutex + take-dance + manual lazy/deep Clone are gone: Clone is a refcount bump, and get(), decompress() and stream() all keep working after each other (previously raw_bytes() returned None after get()).

Public streaming reader. wacore::history_sync::HistorySyncStream iterates a compressed blob with bounded memory (peak ≈ the largest single conversation): next_conversation_bytes() lends raw entry bytes from the inflate window, next_conversation() is the decoded convenience layer and is lenient (a corrupt entry is skipped and counted via skipped_conversations(), not fatal), and remainder() decodes everything that is not a conversation (pushnames, mappings, nctSalt, ...) regardless of wire order. Calling remainder() before exhausting the conversations drains the tail and fails loud with the new HistorySyncError::UnreadConversations if it would silently drop one.

One wire walk. Both the internal extractor (process_history_sync) and the public stream consume the same FieldWalker, so the format knowledge lives in exactly one production place. The old full-decompress parse path was deleted from production and moved into the test module as reference_full_walk, keeping the differential parity oracle alive (production stream vs independent full-buffer implementation) instead of weakening it to a re-derived check.

Extraction always streams. retain_blob == true now just hands the compressed input back in HistorySyncResult::compressed_bytes (a Vec to Bytes move, no copy, no second inflate), so a bot with a catch-all on_event handler no longer forces the whole-blob materialization on every chunk. The unused _compressed_size_hint parameter is gone.

Exact-size inflate caps. The producer-counted decompressed_size is used as the inflate bound for get()/decompress()/stream(), a strictly tighter anti-bomb limit than the global 64 MB ceiling (MAX_DECOMPRESSED, now a pub const for raw HistorySyncStream::new users). InflateReader gained a total_out() getter to expose the count.

Latent panic fixed. Exercising small exact-size caps surfaced a real bug: decompress_zlib_pooled pre-sizes its buffer with .clamp(4096, cap), which panics whenever the cap is below 4096 bytes. It never fired before because the only caller passed 64 MB, but any small-cap call (a PushName-only chunk through the new decompress()) would have hit it. The floor now bows to the cap, with a regression test.

Cost model (documented on the new APIs)

Consumers that decode pay one extra zlib inflate per consumption pass compared to the old retained-decompressed design; in exchange every queued event is ~10x smaller. A multi-MB chunk takes tens of milliseconds to inflate (plus prost decode for get()), so the rustdoc on LazyHistorySync and HistorySyncStream recommends spawn_blocking (cloning compressed_bytes() into the closure) when decompressed_size() is large, mirroring the producer's own 256 KB inline threshold.

Breaking changes and migration

  • LazyHistorySync::new(raw, sync_type, ...) is now new(compressed, decompressed_size, sync_type, ...).
  • raw_bytes() was removed: use decompress() for the inflated bytes (per call, no caching), compressed_bytes() for the stored payload, or stream() for incremental consumption. raw_size() is now decompressed_size().
  • HistorySyncResult::decompressed_bytes was replaced by compressed_bytes plus decompressed_size; process_history_sync lost its unused 4th parameter.
  • Clone semantics: the decode cache is no longer carried over by Clone (a clone re-inflates on demand); Serialize output is unchanged (metadata only).

Tests

44 wacore history-sync tests (stream parity vs full prost decode, field-order-shuffled blobs, conversation-less blobs, lenient decode, zero-length conversation, truncated zlib and truncated length-delimited fields, window growth for a 1 MB conversation, repeated window refills, decompressed cap enforcement, fail-loud early remainder(), unknown wire types, exact size reporting), 12 LazyHistorySync tests (everything-works-after-get, per-call decompress, cheap clone, undersized-cap fail-loud), and a new producer test asserting the dispatched event carries the original compressed payload + exact size and that get()/stream() work end-to-end. The differential corpus and prior extraction tests pass unchanged.

Validation: cargo clippy --workspace --all-targets -- -D warnings clean, full test suites green (whatsapp-rust 984, wacore 799+103, wacore-binary), cargo bench --no-run compiles (the bench dropped the removed parameter and gained a stream_drain consumer-side benchmark), wasm32 --no-default-features build green. Expect CodSpeed movement on bench_process_history_sync: the retain path no longer does the duplicate full-buffer walk, so it should get faster.

Addendum: flamegraph-driven follow-ups (same PR)

CodSpeed flamegraph analysis of the first revision surfaced two more wins, both included here:

Boxed per-message giants (perf(waproto)!). The new stream_drain benchmark exposed that ~94% of a full conversation decode was memcpy: prost's push(default) plus Vec doubling moved HistorySyncMsg elements of 15,680 bytes each (WebMessageInfo inline at 15,664 bytes, with wa::Message inline at 6,504 bytes). HistorySyncMsg.message and WebMessageInfo.message are now generated as Option<Box<...>> (prost_build::Config::boxed), collapsing the element to 24 bytes. Locally the stream drain improves ~25% in wall time; the instruction-count (Simulation) win should be substantially larger. Breaking for constructors (Some(x) becomes Some(Box::new(x))); reads auto-deref.

Single-copy inflate (perf(zlib)). InflateReader::pump inflated into a 64 KB stack chunk and then extend_from_sliced into the window, copying every decompressed byte twice (~10% of an extraction in the instruction profile). It now inflates straight into the window's spare capacity via decompress_vec, the same idiom decompress_zlib_pooled already used.

Also addressed from review: both bots flagged that a zlib stream truncated exactly at a protobuf field boundary passed extraction as clean EOF (the old full-decompress retained path rejected it). InflateReader now tracks a real zlib StreamEnd and the walker requires it at EOF, with a sync-flush-without-finish regression test.

…ing reader

- Event::HistorySync now carries the compressed bytes (~10x smaller), so queued events cost O(compressed) instead of O(decompressed)
- new public HistorySyncStream: conversations one at a time with bounded memory, lenient per-conversation decode, fail-loud remainder()
- the internal extractor and the stream share one wire walk (FieldWalker); the duplicated full-decompress parse path moved to cfg(test) as the parity oracle
- LazyHistorySync loses the Mutex take-dance: get()/decompress()/stream() all keep working after each other, Clone is a refcount bump
- fixes a latent decompress_zlib_pooled clamp panic for caps below 4096 bytes, exposed by exact-size inflate caps
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added streaming support for incremental conversation processing during history synchronization.
  • Improvements

    • Optimized history synchronization to use lazy decompression, reducing memory overhead by retaining compressed data instead of fully decompressed payloads.
    • Enhanced decompression tracking with improved size reporting and stream validation.

Walkthrough

Single-pass streaming history-sync extraction now inflates incrementally, reports exact decompressed_size, optionally retains compressed_bytes, and exposes LazyHistorySync as a compressed payload with on-demand decompress and streaming iteration. Prost build boxed nested message fields; tests and benches updated.

Changes

Streaming Refactor & Boxed-proto Migration

Layer / File(s) Summary
All refactor and migration
wacore/src/history_sync.rs, wacore/src/types/events.rs, wacore/binary/src/zlib_pool.rs, src/history_sync.rs, waproto/build.rs, wacore/benches/*, src/*, wacore/src/*
Single-pass FieldWalker-based process_history_sync using InflateReader windows; HistorySyncResult reports decompressed_size and optional compressed_bytes; LazyHistorySync stores compressed blob and exposes compressed_bytes(), decompressed_size(), decompress(), stream(), and get() caching; InflateReader reports total_out and stream_ended; prost build config boxes nested submessages and call sites/tests updated to use Some(Box::new(...)); runtime dispatch/bench wiring and unit tests adjusted accordingly.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Sync Consumer
  participant FieldWalker
  participant InflateReader
  participant Stream as HistorySyncStream
  participant Remainder as Decode Remainder

  Client->>FieldWalker: pass compressed data + cap
  FieldWalker->>InflateReader: request next inflated window
  InflateReader-->>FieldWalker: inflated window slice (total_out, stream_ended)
  FieldWalker->>FieldWalker: parse top-level field tag/wire-type
  alt conversation field
    FieldWalker->>Stream: yield borrowed conversation bytes
    Stream-->>Client: conversation payload
  else non-conversation field
    FieldWalker->>FieldWalker: buffer raw wire bytes
  end
  Client->>Stream: remainder()
  Stream->>Remainder: decode buffered top-level fields
  Remainder-->>Client: full HistorySync proto
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: storing compressed payload and exposing streaming reader for history-sync, matching the core refactor across multiple files.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering the why, what changed, cost model, breaking changes, and test coverage for the history-sync refactor.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/history-sync-compressed-streaming

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 and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 532527b8c7

ℹ️ 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".

.map_err(HistorySyncError::DecompressionError)?
{
break;
return Ok(None);

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 Reject truncated retained history streams

When the compressed payload is cut off after the last protobuf field but before the zlib stream terminator/checksum, this EOF path accepts the blob as successfully parsed because InflateReader treats input exhaustion as EOF. That was already risky for the non-retained streaming extractor, but this commit now routes retain_blob == true through the same walker, so clients with HistorySync handlers can dispatch a LazyHistorySync containing truncated compressed bytes; later get()/decompress() will fail even though the sync was logged and internal side effects were applied. The retained path should still force/validate zlib StreamEnd before returning success.

Useful? React with 👍 / 👎.

@codspeed-hq

codspeed-hq Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 2 (👁 2) regressed benchmarks
✅ 137 untouched benchmarks
🆕 2 new benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory bench_process_history_sync 5.8 MB 2.5 MB ×2.3
🆕 Memory bench_history_sync_stream_drain N/A 542 KB N/A
🆕 Simulation bench_history_sync_stream_drain N/A 85.2 ms N/A
👁 Memory bench_group_recv 2.8 KB 5.2 KB -45.71%
👁 Simulation bench_unpack_uncompressed 183.3 ns 241.7 ns -24.14%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/history-sync-compressed-streaming (1623c5f) with main (9e8fca9)

Open in CodSpeed

@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.

1 issue found across 5 files

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

Fix all with cubic | Re-trigger cubic

Comment thread wacore/src/history_sync.rs
InflateReader treated input exhaustion as clean EOF, so a blob cut exactly between protobuf fields parsed successfully and (with retain_blob) dispatched an event whose get()/decompress() would fail later. Track zlib StreamEnd explicitly and have the walker require it at EOF, matching the strictness the old full-decompress retained path had. Raised by both review bots on #853.
jlucaso1 added 2 commits June 11, 2026 15:46
The stack chunk + extend_from_slice pair copied every decompressed byte a second time, ~10% of a history-sync extraction in the CodSpeed instruction profile. decompress_vec writes into the window's spare capacity instead (same idiom decompress_zlib_pooled already uses).
…decode path

CodSpeed attributed ~94% of a full history-sync conversation decode to memcpy: prost's push(default) + Vec doubling moved HistorySyncMsg elements of 15,680 bytes each (WebMessageInfo inline at 15,664 with wa::Message inline at 6,504). Boxing HistorySyncMsg.message and WebMessageInfo.message collapses the element to 24 bytes; locally the stream drain drops ~25% in wall time, with a far larger instruction-count win expected.

Breaking: both fields are now Option<Box<...>> (construction sites wrap with Box::new; reads auto-deref).

@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 (3)
wacore/src/history_sync.rs (2)

1633-1647: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t emit a tc-token candidate without a conversation id.

Lines 1639-1647 only guard on tc_token.is_empty(). If a malformed conversation carries TC_TOKEN and timestamps but omits ID, this still returns TcTokenCandidate { id: "" }, which can leak bad data into downstream token state. Gate the return on !chat_id.is_empty() the same way message-secret extraction already does.

Suggested fix
-    if tc_token.is_empty() {
+    if chat_id.is_empty() || tc_token.is_empty() {
         return None;
     }
     Some(TcTokenCandidate {
         id: chat_id.to_string(),
         tc_token: tc_token.to_vec(),
🤖 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/history_sync.rs` around lines 1633 - 1647, The current function
can return a TcTokenCandidate with an empty conversation id; update the return
gating so we only construct and return TcTokenCandidate when chat_id is
non-empty and tc_token is non-empty (i.e., add a check like !chat_id.is_empty()
alongside the existing tc_token.is_empty() guard). Locate the block around
parse_jid_fast(...) and the return Some(TcTokenCandidate { ... }) and ensure you
mirror the message-secret extraction pattern by returning None if chat_id is
empty before creating TcTokenCandidate(id: chat_id.to_string(), ...).

68-75: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Restore the exact inflate cap on the first extraction pass.

Line 73 hard-codes MAX_DECOMPRESSED, so this pass can still inflate and process up to 64 MiB even when the producer already reported a much smaller decompressed_size. After removing _compressed_size_hint from process_history_sync, there is no path left to thread that tighter bound into FieldWalker::new(), so the first pass lost the zip-bomb / over-allocation protection that LazyHistorySync::decompress() applies later.

Based on PR objectives and the downstream LazyHistorySync contract, the producer-reported decompressed size is supposed to be the cap, not just a metric.

🤖 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/history_sync.rs` around lines 68 - 75, The initial extraction pass
currently uses the hard-coded MAX_DECOMPRESSED in process_history_sync when
calling process_history_sync_streaming, which loses the producer-reported
decompressed_size cap; change the call so the producer-reported
decompressed_size (when available) is threaded into
process_history_sync_streaming and ultimately into FieldWalker::new (or
reintroduce a decompressed_size_hint parameter to process_history_sync) so the
first pass enforces the same cap LazyHistorySync::decompress() uses instead of
MAX_DECOMPRESSED; ensure the compressed retain_blob path and returned
HistorySyncResult still work with the new parameter.
src/history_sync.rs (1)

144-148: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

This snapshots HistorySync interest too early.

At Lines 144-148 you decide whether to retain the blob before process_history_sync starts. CoreEventBus says interest is re-checked at dispatch time, but this path makes Event::HistorySync impossible to materialize if a handler widens to EventKind::HistorySync during the parse window. On large blobs that window is real because Lines 160-171 push the work into spawn_blocking, and Lines 217-231 only dispatch when compressed_bytes was retained up front. Either carry the compressed blob through dispatch-time interest evaluation, or explicitly narrow the bus contract for HistorySync.

🤖 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/history_sync.rs` around lines 144 - 148, The code snapshots HistorySync
interest too early by evaluating retain_history_blob =
self.core.event_bus.has_handler_for(EventKind::HistorySync) before
process_history_sync and then dropping compressed_bytes unless that pre-check
passed; instead, change the logic so compressed_bytes is carried through to
dispatch-time interest evaluation (or explicitly narrow the contract) — i.e.,
remove/avoid using the early retain_history_blob flag, pass compressed_bytes
along into the spawn_blocking/dispatch path, and call
self.core.event_bus.has_handler_for(EventKind::HistorySync) (or use the bus’s
dispatch-time check) right before creating/dispatching Event::HistorySync in
process_history_sync so a handler that registers during parsing can still cause
the blob to be materialized.
🤖 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/history_sync.rs`:
- Around line 144-148: The code snapshots HistorySync interest too early by
evaluating retain_history_blob =
self.core.event_bus.has_handler_for(EventKind::HistorySync) before
process_history_sync and then dropping compressed_bytes unless that pre-check
passed; instead, change the logic so compressed_bytes is carried through to
dispatch-time interest evaluation (or explicitly narrow the contract) — i.e.,
remove/avoid using the early retain_history_blob flag, pass compressed_bytes
along into the spawn_blocking/dispatch path, and call
self.core.event_bus.has_handler_for(EventKind::HistorySync) (or use the bus’s
dispatch-time check) right before creating/dispatching Event::HistorySync in
process_history_sync so a handler that registers during parsing can still cause
the blob to be materialized.

In `@wacore/src/history_sync.rs`:
- Around line 1633-1647: The current function can return a TcTokenCandidate with
an empty conversation id; update the return gating so we only construct and
return TcTokenCandidate when chat_id is non-empty and tc_token is non-empty
(i.e., add a check like !chat_id.is_empty() alongside the existing
tc_token.is_empty() guard). Locate the block around parse_jid_fast(...) and the
return Some(TcTokenCandidate { ... }) and ensure you mirror the message-secret
extraction pattern by returning None if chat_id is empty before creating
TcTokenCandidate(id: chat_id.to_string(), ...).
- Around line 68-75: The initial extraction pass currently uses the hard-coded
MAX_DECOMPRESSED in process_history_sync when calling
process_history_sync_streaming, which loses the producer-reported
decompressed_size cap; change the call so the producer-reported
decompressed_size (when available) is threaded into
process_history_sync_streaming and ultimately into FieldWalker::new (or
reintroduce a decompressed_size_hint parameter to process_history_sync) so the
first pass enforces the same cap LazyHistorySync::decompress() uses instead of
MAX_DECOMPRESSED; ensure the compressed retain_blob path and returned
HistorySyncResult still work with the new parameter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3929c703-074f-4e54-961b-e1113c7b37ba

📥 Commits

Reviewing files that changed from the base of the PR and between 5f17b29 and f9fbe17.

📒 Files selected for processing (7)
  • src/history_sync.rs
  • src/pdo.rs
  • wacore/benches/history_sync_benchmark.rs
  • wacore/binary/src/zlib_pool.rs
  • wacore/src/history_sync.rs
  • wacore/src/types/events.rs
  • waproto/build.rs

…mpty-id tctoken candidates

The pre-parse has_handler_for snapshot dated from when retain meant a full decompress; now that retaining is a free move of the compressed input, snapshotting early only made a handler registered during a long parse silently miss the event. The blob is always carried through and interest is checked right before dispatch.

extract_conversation_fields could also return a TcTokenCandidate with an empty conversation id (the downstream JID parse dropped it, but the API should not produce it); it now mirrors the message-secret empty-id guard. A third reviewer suggestion (threading a producer decompressed-size cap into the first pass) was skipped: that size does not exist before the parse, it is what the extraction pass counts.

@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/history_sync.rs`:
- Around line 214-219: Remove the early dispatch-time gate using
self.core.event_bus.has_handler_for(EventKind::HistorySync); instead only check
for sync_result.compressed_bytes, construct the LazyHistorySync from that
compressed payload, and call self.core.event_bus.dispatch(...) so
CoreEventBus::dispatch() performs the final interest check. This eliminates the
missed-event race by deferring the handler-interest decision to
CoreEventBus::dispatch and keeps references to sync_result.compressed_bytes and
LazyHistorySync intact.
🪄 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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3bd99332-3ff3-4ef7-9276-4e7b2134bcdb

📥 Commits

Reviewing files that changed from the base of the PR and between f9fbe17 and 288a18b.

📒 Files selected for processing (2)
  • src/history_sync.rs
  • wacore/src/history_sync.rs

Comment thread src/history_sync.rs Outdated
Comment thread wacore/src/history_sync.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.

1 issue found across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread src/history_sync.rs Outdated
The dispatch-time has_handler_for pre-check read a different bus snapshot than dispatch() itself, leaving a check-to-dispatch window where a freshly registered handler lost the event — the same race class the previous commit removed, just narrower. dispatch() already evaluates interest against a single snapshot and skips materializing the Arc when nobody listens, and building the event is only a Bytes refcount move, so the pre-check bought nothing.
Second boxing wave, found via a full size_of probe of every inline field: WebMessageInfo.statusMentionMessageInfo carries a wa::Message inline and was most of WebMessageInfo's remaining bulk; messageContextInfo was the only large inline left in wa::Message. Drops the per-message default-construct copy from ~15.7 KB to ~6.5 KB (WebMessageInfo 9168 -> 2672 B, Message 6504 -> 3784 B). Wire and serde shapes are unchanged; constructors wrap with Box::new, reads auto-deref.
@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.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

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

377-386: ⚠️ Potential issue | 🟡 Minor

Fix unwrap_device_sent to use a let-chain (no nested if let)

wacore/src/messages.rs still uses nested if let inside unwrap_device_sent (the device_sent_message/dsm.message unwrap), which violates the let-chain guideline. Convert it to a single let-chain while preserving the current behavior (return the inner message when dsm.message exists; otherwise keep the wrapper).

Proposed refactor
 pub fn unwrap_device_sent(mut msg: wa::Message) -> wa::Message {
-    if let Some(mut dsm) = msg.device_sent_message.take() {
-        if let Some(mut inner) = dsm.message.take() {
-            inner.message_context_info = crate::proto_helpers::merge_dsm_context(
-                inner.message_context_info.take(),
-                msg.message_context_info.as_deref(),
-            );
-            return *inner;
-        }
-        msg.device_sent_message = Some(dsm);
+    if let Some(dsm) = msg.device_sent_message.as_mut()
+        && let Some(mut inner) = dsm.message.take()
+    {
+        inner.message_context_info = crate::proto_helpers::merge_dsm_context(
+            inner.message_context_info.take(),
+            msg.message_context_info.as_deref(),
+        );
+        return *inner;
     }
     msg
 }
🤖 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/messages.rs` around lines 377 - 386, Refactor unwrap_device_sent
to remove nested if-let by first taking msg.device_sent_message into a local
variable (e.g., let mut dsm_opt = msg.device_sent_message.take()), then use a
single let-chain: if let Some(mut dsm) = dsm_opt && let Some(mut inner) =
dsm.message.take() { update inner.message_context_info via
crate::proto_helpers::merge_dsm_context and return *inner; } else if let
Some(dsm) = dsm_opt { restore msg.device_sent_message = Some(dsm); } — this
preserves the existing behavior around msg.device_sent_message, dsm.message,
merge_dsm_context, and inner.message_context_info while complying with the
let-chain guideline.

Source: Coding guidelines

🤖 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 `@wacore/src/messages.rs`:
- Around line 377-386: Refactor unwrap_device_sent to remove nested if-let by
first taking msg.device_sent_message into a local variable (e.g., let mut
dsm_opt = msg.device_sent_message.take()), then use a single let-chain: if let
Some(mut dsm) = dsm_opt && let Some(mut inner) = dsm.message.take() { update
inner.message_context_info via crate::proto_helpers::merge_dsm_context and
return *inner; } else if let Some(dsm) = dsm_opt { restore
msg.device_sent_message = Some(dsm); } — this preserves the existing behavior
around msg.device_sent_message, dsm.message, merge_dsm_context, and
inner.message_context_info while complying with the let-chain guideline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0a604161-9107-4b87-8342-803e2e01cd68

📥 Commits

Reviewing files that changed from the base of the PR and between 288a18b and a835413.

📒 Files selected for processing (14)
  • src/client/messaging.rs
  • src/features/comments.rs
  • src/features/events.rs
  • src/features/polls.rs
  • src/history_sync.rs
  • src/message/tests.rs
  • src/send.rs
  • wacore/benches/history_sync_benchmark.rs
  • wacore/src/history_sync.rs
  • wacore/src/messages.rs
  • wacore/src/msg_secret.rs
  • wacore/src/proto_helpers.rs
  • wacore/src/reporting_token.rs
  • waproto/build.rs

jlucaso1 added 2 commits June 11, 2026 18:07
…xtractor

10k seeded byte flips/truncations of a valid blob must never panic on either consumption path, only clean errors or lenient skips. Added during an adversarial review pass of the PR.
@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.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

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

377-383: ⚠️ Potential issue | 🟡 Minor

Collapse the nested if let guards in unwrap_device_sent into a single let-chain.

unwrap_device_sent still uses nested if let (device_sent_message.take() then dsm.message.take()), so it violates the repo’s collapsible_if/let-chains rule. Convert it to a let-chain while keeping the current behavior that re-attaches dsm back onto msg.device_sent_message when dsm.message is None.

🤖 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/messages.rs` around lines 377 - 383, Replace the nested if-let in
unwrap_device_sent by first doing let mut dsm_opt =
msg.device_sent_message.take(); then use a let-chain: if let Some(mut dsm) =
dsm_opt && let Some(mut inner) = dsm.message.take() { update
inner.message_context_info via crate::proto_helpers::merge_dsm_context and
return *inner; } else if let Some(dsm) = dsm_opt { re-attach it with
msg.device_sent_message = Some(dsm); } — this preserves behavior while
collapsing the nested guards (references: unwrap_device_sent,
msg.device_sent_message, dsm, dsm.message, inner,
crate::proto_helpers::merge_dsm_context).

Source: Coding guidelines

🤖 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 `@wacore/src/messages.rs`:
- Around line 377-383: Replace the nested if-let in unwrap_device_sent by first
doing let mut dsm_opt = msg.device_sent_message.take(); then use a let-chain: if
let Some(mut dsm) = dsm_opt && let Some(mut inner) = dsm.message.take() { update
inner.message_context_info via crate::proto_helpers::merge_dsm_context and
return *inner; } else if let Some(dsm) = dsm_opt { re-attach it with
msg.device_sent_message = Some(dsm); } — this preserves behavior while
collapsing the nested guards (references: unwrap_device_sent,
msg.device_sent_message, dsm, dsm.message, inner,
crate::proto_helpers::merge_dsm_context).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a0cda85f-3e34-4ea9-81f1-adc9670e7727

📥 Commits

Reviewing files that changed from the base of the PR and between 288a18b and 1623c5f.

📒 Files selected for processing (14)
  • src/client/messaging.rs
  • src/features/comments.rs
  • src/features/events.rs
  • src/features/polls.rs
  • src/history_sync.rs
  • src/message/tests.rs
  • src/send.rs
  • wacore/benches/history_sync_benchmark.rs
  • wacore/src/history_sync.rs
  • wacore/src/messages.rs
  • wacore/src/msg_secret.rs
  • wacore/src/proto_helpers.rs
  • wacore/src/reporting_token.rs
  • waproto/build.rs

@jlucaso1
jlucaso1 merged commit eecda89 into main Jun 11, 2026
13 checks passed
@jlucaso1
jlucaso1 deleted the perf/history-sync-compressed-streaming branch June 11, 2026 21:26
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