Skip to content

perf(history-sync): size the secret-record accumulator by sampled density - #945

Merged
jlucaso1 merged 3 commits into
mainfrom
perf/history-sync-secret-records-reserve
Jul 2, 2026
Merged

perf(history-sync): size the secret-record accumulator by sampled density#945
jlucaso1 merged 3 commits into
mainfrom
perf/history-sync-secret-records-reserve

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Sizes the history-sync secret-record accumulator from a sampled record density instead of growing it by plain doubling.

Problem

msg_secret_records grew from empty by repeated doubling, which copies roughly 2x the final byte volume through realloc per chunk. Profiling the history-sync scenario (300 convs x 20 msgs, 4 chunks) showed this single site was the client's largest allocation-churn source, and CodSpeed's flamegraph attributed 5.6% of bench_process_history_sync to the resulting realloc+memcpy chain.

A full pre-count pass was already tried and removed in #690: it re-scanned the whole blob and cost ~2.5% of the decode while over-allocating. This takes a third route: once the accumulator reaches 128 records, extrapolate the density observed so far to the blob's estimated decompressed size (from the zlib input ratio) and reserve once. O(1), no second pass; an under-estimate just resumes doubling, and the clamp bounds over-allocation to ~2 MB. The 128-record sample is load-bearing: extrapolating from the first conversation alone overshot to the clamp and doubled the transient peak on the same workload.

Before / after

Production harness (whatsapp-benchs history-sync, 300x20, MODE=dhat):

metric before after delta
record-site bytes through allocator 9.96 MB 4.53 MB -55%
record-site allocs 48 32 -33%
record-site transient peak 1245 KB 1056 KB -15%
client total allocation churn 27.1 MB 21.8 MB -20%
client peak (t-gmax) 5.59 MB 5.39 MB -3.5%

Micro (bench_process_history_sync, 500x40 fixture, valgrind dhat on the record site): 4.98 MB / 13 allocs -> 1.89 MB / 8 allocs.

Wall time on the same bench is neutral-to-slightly-better under an interleaved A/B (baseline ~8.1 ms vs patched ~7.7 ms median-of-3, within machine noise); the deterministic instruction-count delta will show in this PR's CodSpeed run.

Regressions considered

  • Blobs with fewer than 128 records never reserve and keep the exact previous behavior; the ladder up to 128 records copies ~35 KB total.
  • A wrong density estimate can only over-reserve up to the 16384-record clamp (~2 MB transient); the sampled estimate landed within ~2x of the true count on both measured workloads, vs ~10x overshoot when sampling a single conversation (that variant was measured and rejected).

Review in cubic

…sity

The msg_secret_records Vec grew by plain doubling, which copies about twice
the final byte volume through realloc per chunk; on a measured 300x20
history sync that site alone moved 10 MB across 48 reallocs (37% of the
client's total allocation churn) and cost 5.6% of the decode on the realloc
plus memcpy path.

A full pre-count pass was already tried and removed (#690): it re-scanned
the whole blob and cost ~2.5% of the decode. Instead, once the accumulator
reaches 128 records, extrapolate the record density observed so far to the
blob's estimated decompressed size (zlib input ratio) and reserve once.
O(1), no second pass; a low estimate just resumes doubling, and the clamp
bounds over-allocation to ~2 MB. Sampling at 128 matters: extrapolating
from the first conversation alone overshot to the clamp and doubled the
transient peak on the same workload.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jlucaso1, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 32b36d47-7015-4a26-b550-ffcc7aef587e

📥 Commits

Reviewing files that changed from the base of the PR and between a3c0cea and 2ca8030.

📒 Files selected for processing (1)
  • wacore/src/history_sync.rs
📝 Walkthrough

Walkthrough

Adds a compressed_progress() method to InflateReader exposing compressed input position and total length. FieldWalker gains parsed_bytes() and estimated_total_out() helpers built on this to extrapolate decompressed size, which process_history_sync_streaming uses to pre-reserve capacity for the secret-records vector.

Changes

Decompression Progress and Capacity Reservation

Layer / File(s) Summary
Compressed progress accessor
wacore/binary/src/zlib_pool.rs
Adds compressed_progress() returning current input position and total input length as a (usize, usize) tuple.
Progress estimation helpers
wacore/src/history_sync.rs
Adds parsed_bytes() and estimated_total_out() to FieldWalker, computing consumed decompressed bytes and extrapolating total output size from the compressed/decompressed ratio.
Secret-record capacity reservation
wacore/src/history_sync.rs
Introduces RESERVE_SAMPLE_RECORDS and RECORD_RESERVE_CAP constants; once a sample threshold of secret records is collected, estimates the final record count and reserves capacity on result.msg_secret_records once.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#672: Introduced the streaming InflateReader decompression path that this PR extends with progress tracking used in history_sync.
  • oxidezap/whatsapp-rust#683: Modifies InflateReader internals around decompression progress state that this PR's compressed_progress() builds on.
  • oxidezap/whatsapp-rust#933: Also touches InflateReader in zlib_pool.rs, migrating inflate mechanics that this PR's new accessor depends on.

Alright, listen up — this is exactly the kind of ruthless efficiency I want to see. We measured. We extrapolated. We pre-reserved capacity so the vector isn't reallocating like it's 2004 dial-up. Small diff, high signal. Ship it, but I want the numbers to be right — no hand-waving on that ratio math.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the sampled-density sizing optimization for history-sync.
Description check ✅ Passed The description matches the changeset and explains the history-sync reserve strategy and its motivation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/history-sync-secret-records-reserve

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.

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

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

Comment thread wacore/src/history_sync.rs Outdated
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the msg_secret_records allocator's naive doubling strategy with a one-shot density-based reserve call, reducing allocation churn in the history-sync hot path by ~55% of bytes and ~33% of allocs.

  • zlib_pool.rs: Adds compressed_progress() to expose the compressed input consumed vs total, enabling callers to extrapolate a decompressed-size estimate mid-stream without a second pass.
  • history_sync.rs: Adds parsed_bytes() and estimated_total_out() to FieldWalker, then applies a one-shot reserve (capped at 16 384 records / ~2 MB) once 128 records accumulate; blobs below that threshold retain the previous doubling behavior unchanged.
  • Two new unit tests validate the single-large-conversation edge case (non-clamping) and the exact-at-drain property of the ratio estimator.

Confidence Score: 5/5

Safe to merge — the reservation path is fully opt-in, the clamp bounds any over-reservation to ~2 MB, and under-estimates fall back to existing doubling behavior.

The arithmetic is overflow-safe (saturating_mul + clamp), the two helpers are private and well-tested, and blobs under the sample threshold see exactly the previous allocation behavior.

No files require special attention.

Important Files Changed

Filename Overview
wacore/binary/src/zlib_pool.rs Adds compressed_progress() -> (usize, usize) exposing (in_pos, input.len()) so callers can compute the compression ratio mid-stream. Minimal, correct addition with no impact on existing inflate logic.
wacore/src/history_sync.rs Adds parsed_bytes() and estimated_total_out() helpers to FieldWalker, then uses them for a one-shot density-based reserve once 128 secret records are accumulated. Logic is sound: overflow is bounded by saturating_mul, the clamp keeps over-allocation to ≤2 MB, and underestimates safely fall back to doubling. Two new targeted tests cover the key edge cases. Style-only nit: some new comments explain what rather than why (AGENTS.md).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[next_field CONVERSATIONS] --> B[extract_conversation_fields push secret records]
    B --> C{density_reserved?}
    C -- yes --> G[continue loop]
    C -- no --> D{records >= 128?}
    D -- no --> G
    D -- yes --> E[density_reserved = true, estimated = records x estimated_total_out / parsed]
    E --> F[estimated = estimated.min 16384, reserve estimated minus records]
    F --> G
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[next_field CONVERSATIONS] --> B[extract_conversation_fields push secret records]
    B --> C{density_reserved?}
    C -- yes --> G[continue loop]
    C -- no --> D{records >= 128?}
    D -- no --> G
    D -- yes --> E[density_reserved = true, estimated = records x estimated_total_out / parsed]
    E --> F[estimated = estimated.min 16384, reserve estimated minus records]
    F --> G
Loading

Reviews (2): Last reviewed commit: "docs(history-sync): update the accumulat..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.22 MiB 10.22 MiB +448 B (+0.00%) 🔺
bin .text 8.26 MiB 8.26 MiB +448 B (+0.01%) 🔺
bin allocated (text+data+bss) 10.22 MiB 10.22 MiB 0
llvm-lines wacore 649,591 649,751 +160 (+0.02%) 🔺
llvm-lines wacore copies 18,006 18,010 +4 (+0.02%) 🔺
llvm-lines whatsapp-rust lib 681,373 681,373 0
llvm-lines whatsapp-rust lib copies 21,088 21,088 0
deps crates (Cargo.lock) 467 467 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.56 MiB 1.56 MiB 0
.text wacore 545.46 KiB 545.86 KiB +414 B (+0.07%) 🔺
.text wacore_binary 157.79 KiB 157.79 KiB 0
.text wacore_libsignal 165.86 KiB 165.86 KiB 0
.text wacore_appstate 144.29 KiB 144.29 KiB 0
.text wacore_noise 27.71 KiB 27.71 KiB 0
.text waproto 871.99 KiB 871.99 KiB 0
.text whatsapp_rust_sqlite_storage 476.89 KiB 476.89 KiB 0
.text whatsapp_rust_tokio_transport 43.57 KiB 43.57 KiB 0
.text whatsapp_rust_ureq_http_client 8.81 KiB 8.81 KiB 0
.text std 1005.68 KiB 1005.68 KiB 0
.text other deps 3.29 MiB 3.29 MiB 0
Top movers (cargo-bloat attribution)
Crate main PR Δ
regex_automata 1.63 KiB 2.90 KiB +1.26 KiB (+77.33%)
prost 371.90 KiB 370.63 KiB -1.26 KiB (-0.34%)

Baseline: 96686eac6 (latest main run) · Head: 120ce80f9 · 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: 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 `@wacore/src/history_sync.rs`:
- Around line 116-138: Add unit tests for the arithmetic in history_sync’s
`parsed_bytes` and `estimated_total_out` helpers so the ratio extrapolation and
guard behavior stay locked down. Use `FieldWalker` (or the same decompression
path exercised by `compressed_progress`, `total_out`, and `available`) to cover
a fully drained stream where `estimated_total_out()` equals `total_out()`, and a
partial-progress case where it extrapolates correctly from known compressed
input. Also include an edge-case assertion for zero compressed progress to
ensure the `in_pos == 0` branch remains safe.
- Around line 304-316: In the density estimate logic inside the history sync
reservation block, `parsed` is already clamped with `.max(1)`, so the
`checked_div(...).map_or(records, ...)` fallback is unreachable and misleading.
Simplify the estimate calculation in the `density_reserved` branch by removing
the unnecessary `checked_div` fallback path and keeping the `records`,
`walker.parsed_bytes()`, `walker.estimated_total_out()`, and
`RECORD_RESERVE_CAP` flow 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 (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: de335061-2d4d-42df-819d-9e644e6bae57

📥 Commits

Reviewing files that changed from the base of the PR and between 96686ea and a3c0cea.

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

Comment thread wacore/src/history_sync.rs Outdated
Comment thread wacore/src/history_sync.rs
@codspeed-hq

codspeed-hq Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by 54.77%

⚡ 1 improved benchmark
✅ 178 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory bench_process_history_sync 2.5 MB 1.6 MB +54.77%

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-secret-records-reserve (2ca8030) with main (96686ea)

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.

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="wacore/src/history_sync.rs">

<violation number="1" location="wacore/src/history_sync.rs:306">
P2: The density denominator (`parsed_bytes()`) may exclude the current conversation's payload because `pending_payload` documentation states the yielded field's bytes stay buffered until the next `next_field()` call — meaning they remain part of `available()` and are subtracted from `total_out()` in `parsed_bytes()`. Meanwhile, `records` already includes secrets extracted from that conversation. This inflates the apparent record density, causing the estimate to overshoot (potentially hitting `RECORD_RESERVE_CAP` of 16384) and reserving up to ~2 MB for blobs that may contain far fewer records.

Consider computing the sample after the field is fully consumed, or adding the current field's span to the parsed-byte count before using it as the denominator.</violation>

<violation number="2" location="wacore/src/history_sync.rs:311">
P3: Since `parsed` is guaranteed to be at least 1 (from `.max(1)` above), the `checked_div(parsed)` call can never return `None`, making the `map_or(records, ...)` fallback unreachable. This reads as if there's a live division-by-zero path when there isn't one. Consider using a plain division instead, or adding a comment noting the guard is purely defensive.</violation>
</file>

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

Re-trigger cubic

Comment thread wacore/src/history_sync.rs
Comment thread wacore/src/history_sync.rs Outdated
jlucaso1 added 2 commits July 2, 2026 15:48
parsed_bytes excluded the whole unparsed buffer, but the field most
recently yielded by next_field stays buffered until the following call
even though its records were already extracted. On a blob whose first
conversation alone reaches the sample threshold the denominator was
near zero, so the estimate clamped and over-reserved ~2 MB. Count the
pending field, drop the unreachable checked_div fallback, and pin both
behaviors with tests (single-big-conversation reserve, estimator
exactness after a full drain).
…serve

The initializer still described plain growth as the strategy; it now points
at the density-based reserve below and keeps the pre-count history.

@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: 2ca8030d21

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

Comment thread wacore/src/history_sync.rs
@jlucaso1
jlucaso1 merged commit e616863 into main Jul 2, 2026
18 checks passed
@jlucaso1
jlucaso1 deleted the perf/history-sync-secret-records-reserve branch July 2, 2026 19:01
jlucaso1 added a commit that referenced this pull request Jul 2, 2026
Resolve the history_sync.rs conflicts by taking main's side (the #945
density reserve comment and the #947 Option-returning read_varint), then
adapt the cross-pollinated tests: the #945 single-big-conversation test
builds MessageKey via buffa::MessageField::some, and the branch's varint
overflow test asserts is_none() instead of a Result error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant