Skip to content

perf(upload): slice ciphertext zero-copy instead of copying per attempt - #774

Merged
jlucaso1 merged 2 commits into
mainfrom
perf/upload-zero-copy-bytes-body
Jun 8, 2026
Merged

perf(upload): slice ciphertext zero-copy instead of copying per attempt#774
jlucaso1 merged 2 commits into
mainfrom
perf/upload-zero-copy-bytes-body

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

The legacy in-memory upload closure ran ciphertext[offset..].to_vec() on every send attempt — a full copy of the (potentially multi-MB) encrypted media payload on the common first attempt (offset 0). It was forced only because HttpRequest::with_body took Vec<u8>; the closure is re-callable for retry/resume so the ciphertext can't be moved out, and the Vec<u8> body type required an owned copy.

This makes HttpRequest.body a bytes::Bytes (with_body now takes impl Into<Bytes>, so existing Vec<u8> callers are unchanged) and holds the ciphertext as Bytes, so each attempt becomes ciphertext.slice(offset..) — a refcount bump, no copy. The ureq client reads the body via &body[..] (Bytes derefs to [u8]), so it needs no change.

upload_stream already avoids the copy for large media; this fixes the buffered upload() path that's still the public API for in-memory media. Pure perf/type change; the existing upload retry/failover tests exercise the multi-attempt body path.

The legacy in-memory upload closure did ciphertext[offset..].to_vec() on
every send attempt — a full copy of the (potentially multi-MB) encrypted
media payload on the common first attempt (offset 0), forced only because
HttpRequest::with_body took Vec<u8>.

Make HttpRequest.body a bytes::Bytes (with_body now takes impl Into<Bytes>,
so existing Vec<u8> callers are unchanged) and hold the ciphertext as Bytes,
so each attempt is ciphertext.slice(offset..) — a refcount bump, no copy.
The ureq client reads the body via &body[..] (Bytes derefs to [u8]), so it
needs no change. upload_stream already avoids this for large media; this
fixes the buffered upload() path used for in-memory media.
@coderabbitai

coderabbitai Bot commented Jun 8, 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

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5d84e8ca-be66-4400-b6a1-61cf7eae572d

📥 Commits

Reviewing files that changed from the base of the PR and between 3fb8d45 and fa817b1.

📒 Files selected for processing (1)
  • tests/e2e/src/lib.rs

📝 Walkthrough

Summary by CodeRabbit

  • Refactor

    • Reduced memory copying during upload retry/resume to improve performance and reduce peak memory.
    • Switched request body handling to a more efficient bytes-backed representation for HTTP requests, improving request construction and transmission.
  • Tests / Examples

    • Updated example auto-responder to align with the new request payload handling.

Walkthrough

The PR migrates HTTP request bodies from Vec<u8> to bytes::Bytes, updates HttpRequest::with_body to accept impl Into<Bytes>, makes upload retry/resume zero-copy by slicing Bytes, and updates examples/tests to construct bodies with .into().

Changes

Bytes-based HTTP request bodies

Layer / File(s) Summary
HttpRequest contract update
wacore/src/net.rs
HttpRequest.body changes to Option<Bytes> and with_body() now takes impl Into<Bytes>.
Upload path retry/resume optimization
src/upload.rs
Client::upload wraps encrypted payload in Bytes; send_body slices ciphertext.slice(offset..) and passes the slice to request.with_body(...) instead of allocating a new Vec.
Example and test callsite updates
examples/benchmark.rs, tests/e2e/src/lib.rs
QR autoresponder and benchmark example convert QR bytes to the new body type using .into() when building HttpRequest.

Sequence Diagram(s)

sequenceDiagram
  participant Upload as Client::upload
  participant Ciphertext as Bytes~ciphertext~
  participant Closure as send_body
  participant Request as HttpRequest
  Upload->>Ciphertext: wrap enc.data_to_upload as Bytes
  Closure->>Ciphertext: slice(offset..) zero-copy
  Closure->>Request: with_body(sliced_bytes)
  Request->>Request: stores Option~Bytes~
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main performance optimization—avoiding zero-copy slicing instead of copying the ciphertext on each upload retry attempt.
Description check ✅ Passed The description comprehensively explains the problem, the solution using bytes::Bytes, API compatibility, and impact on both buffered and streaming upload paths.
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/upload-zero-copy-bytes-body

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: 3fb8d45bf5

ℹ️ 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/net.rs
pub method: String, // "GET" or "POST"
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
pub body: Option<Bytes>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Convert direct HttpRequest bodies to Bytes

When the full workspace is checked, tests/e2e/src/lib.rs still constructs HttpRequest { body: Some(code.as_bytes().to_vec()) }; after this public field changes to Option<Bytes>, that crate no longer type-checks. The patch updates the same direct construction in examples/benchmark.rs but misses the e2e helper, so cargo check --all/cargo test --all fail before any tests run.

Useful? React with 👍 / 👎.

@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)
examples/benchmark.rs (1)

117-122: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider using the builder pattern for consistency.

Look, the explicit .into() works fine, but you're mixing direct struct construction with the builder pattern elsewhere in the codebase. The builder eliminates the need for that conversion call and keeps things cleaner:

-                                    let req = HttpRequest {
-                                        url: url.clone(),
-                                        method: "POST".into(),
-                                        headers: HashMap::new(),
-                                        body: Some(code.as_bytes().to_vec().into()),
-                                    };
+                                    let req = HttpRequest::post(url.clone())
+                                        .with_body(code.as_bytes().to_vec());

The with_body method handles the conversion internally, which is the whole point of having it. Plus it's more maintainable when we make API changes like this Bytes migration.

🤖 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 `@examples/benchmark.rs` around lines 117 - 122, The HttpRequest construction
here uses direct field initialization with body:
Some(code.as_bytes().to_vec().into()) which mixes styles with the builder used
elsewhere; replace this with the HttpRequest builder (call new() or the existing
builder for HttpRequest) and use the with_body(...) helper (which handles Bytes
conversion) instead of manually converting the body, updating the code that
creates req to use HttpRequest's builder and with_body to keep style and
conversion centralized.
🤖 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 `@examples/benchmark.rs`:
- Around line 117-122: The HttpRequest construction here uses direct field
initialization with body: Some(code.as_bytes().to_vec().into()) which mixes
styles with the builder used elsewhere; replace this with the HttpRequest
builder (call new() or the existing builder for HttpRequest) and use the
with_body(...) helper (which handles Bytes conversion) instead of manually
converting the body, updating the code that creates req to use HttpRequest's
builder and with_body to keep style and conversion centralized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 206f2714-2b1b-4d95-ab89-8d7e923e3795

📥 Commits

Reviewing files that changed from the base of the PR and between db753be and 3fb8d45.

📒 Files selected for processing (3)
  • examples/benchmark.rs
  • src/upload.rs
  • wacore/src/net.rs

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

Benchmark Results

67 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 2,925 2,925 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 8,446 8,446 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 31,317 31,317 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 13,827 13,827 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 49,485 49,485 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 55,001 55,001 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 1,679 1,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 4,393 4,393 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 113,215 112,962 +0.2%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,618 1,656,619 -0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 651,595 651,696 -0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 875,661 875,871 -0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 2,083,494 2,083,623 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 749,074 749,074 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,329,819 1,326,001 +0.3%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,375,877 4,375,752 +0.0%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 514,488 519,032 -0.9%
binary_benchmark::marshal_group::bench_marshal_allocating 45,381 45,381 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 45,431 45,431 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 66,334 66,334 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 43,492 43,492 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 45,487 45,487 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 4,945 4,945 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 4,976 4,976 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 6,747 6,747 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 528,544 528,544 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 528,165 528,165 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 529,411 529,411 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 5,417,732 5,417,732 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 5,362,047 5,362,047 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 13,276,365 13,276,365 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 1,850 1,850 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 29,217 29,217 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 618 618 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 672,890 672,890 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 3,736 3,736 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 3,840 3,840 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 48,274 48,274 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 3,866 3,866 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 48,335 48,335 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 5,206 5,206 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 66,659 66,659 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 310,312 310,312 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 8,291 8,291 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 254 254 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 91 91 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 292 292 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 137 137 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 317 317 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 145 145 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 27,425 27,425 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 10,725 10,725 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 4,139,121 4,141,188 -0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,133 100,133 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 4,264,189 4,264,189 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 100,399 100,399 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 210,262 210,262 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 496,921 496,908 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 508,299 507,734 +0.1%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,974,846 11,979,299 -0.0%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 2,466,138 2,466,138 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 4,911,732 4,888,842 +0.5%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,043,397 2,043,397 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 37,404 37,404 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 3,617,967 3,617,967 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 230,658 230,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 9,980,959 9,980,959 +0.0%
No significant changes detected.

The e2e helper builds an HttpRequest struct literal directly; its body
field is now Option<Bytes>, so the Vec<u8> needs .into(). Missed in the
first pass (the benchmark example had the same literal and was fixed).
This is what failed Build & Test / E2E / Integration Benchmark on the PR.
@jlucaso1
jlucaso1 merged commit 7233f06 into main Jun 8, 2026
10 checks passed
@jlucaso1
jlucaso1 deleted the perf/upload-zero-copy-bytes-body branch June 8, 2026 16:30
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