Skip to content

perf: zero-copy frame send, direct prekey encoding, covariant AttrsRef - #552

Merged
jlucaso1 merged 15 commits into
mainfrom
perf/remaining-alloc-optimizations
Apr 15, 2026
Merged

perf: zero-copy frame send, direct prekey encoding, covariant AttrsRef#552
jlucaso1 merged 15 commits into
mainfrom
perf/remaining-alloc-optimizations

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Apr 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Three allocation optimizations targeting remaining hotspots from DHAT profiling, building on PR #551.

Combined impact (both PRs, DHAT full session):

Metric Original Now Delta
Total bytes allocated 37.9 MB 28.5 MB -9.4 MB (-24.4%)
Total heap blocks 158K 89K -69K (-43.6%)
Connect-to-ready allocs 21,420 9,818 -11,602 (-54.2%)
Connect-to-ready bytes 5.19 MB 3.51 MB -1.68 MB (-32.4%)

This PR alone:

Metric Before After Delta
DHAT total bytes 29.1 MB 28.5 MB -501 KB (-1.7%)
DHAT total blocks 103K 89K -14K (-13.4%)
Connect allocs 12,274 9,818 -2,456 (-20.0%)
iai-callgrind benchmarks baseline 4 improvements, 0 regressions clean

Changes

1. Zero-copy frame send via BytesMut

  • encode_frame_into is now generic over a FrameBuf trait (works with both Vec<u8> and BytesMut)
  • NoiseSocket sender task uses BytesMut for framing output
  • split().freeze() yields a Bytes view without copying -- the BytesMut retains its allocation
  • Eliminates Bytes::copy_from_slice per outbound message

2. Direct binary encoding for prekey upload IQ

  • Made Encoder, EncodeNode, ByteWriter and key write methods public in wacore-binary
  • Added encode_iq_direct method to IqSpec trait (default returns false)
  • PreKeyUploadSpec implements fast path with custom EncodeNode -- writes the entire <iq> stanza directly via Encoder, bypassing NodeBuilder + marshal
  • Eliminates ~5K Node object allocations and the marshal serialization pass
  • Equivalence test verifies identical bytes vs build_iq + marshal
  • Pre-sized output buffer (812 keys * 40 bytes estimate)

3. Covariant AttrsRef enum

  • Replaces type AttrsRef<'a> = Vec<...> with Empty | Slice(Box<[T]>) enum
  • Box<[T]> is 16 bytes (ptr + len) vs Vec's 24 bytes -- no size regression
  • Zero-attr nodes skip allocation entirely via Empty variant
  • Decoder builds Vec with exact capacity, converts to boxed slice
  • Covariant in lifetime (compatible with yoke::Yokeable), compile-time assertion guards this
  • Manual Yokeable impl with safety documentation

4. DRY + correctness fixes

  • Extracted send_and_wait_iq shared helper (eliminates duplicated send_iq_raw)
  • IqError::EncodeError variant for direct-encode failures (was incorrectly ParseError)
  • execute() reuses pre-generated req_id on fallback path
  • Encoder::new_vec clears buffer before writing format byte
  • write_list_start rejects len > u16::MAX instead of silently truncating
  • Timeout limitation documented in encode_iq_direct trait contract

Test plan

  • cargo test --all (949 tests pass)
  • cargo clippy --all --tests clean (0 warnings)
  • iai-callgrind benchmarks: 4 improvements, 0 regressions
  • Equivalence test: direct encode == build_iq + marshal
  • DHAT profiling: -14K blocks (-13.4%)

Summary by CodeRabbit

  • New Features

    • Expanded public binary encoding API surface and a new optional direct-encode hook for some requests.
    • Added a new public error variant for encoding failures.
  • Performance

    • Faster request serialization via a direct-encode fast path.
    • Improved outgoing buffer handling with zero-copy semantics and generalized frame buffer support.
  • Bug Fixes

    • Keepalive treats request encoding failures as fatal to improve shutdown robustness.
  • Tests

    • Added unit tests validating direct-encode output against existing marshal path.

Make encode_frame_into generic over a FrameBuf trait (implemented for
both Vec<u8> and BytesMut). Change the NoiseSocket sender_task's
out_buf from Vec<u8> to BytesMut.

split().freeze() yields a Bytes view of the written frame data while
the BytesMut retains its underlying allocation for the next frame.
This eliminates the per-frame Bytes::copy_from_slice that was copying
~1KB per outbound message.
Add encode_iq_direct fast path to IqSpec trait. PreKeyUploadSpec
implements it with a custom EncodeNode that writes the entire <iq>
stanza directly via the Encoder, bypassing NodeBuilder and marshal.

This eliminates 812 Node object allocations (4,872 blocks) and the
marshal_auto serialization pass for the prekey upload IQ. The encoder
writes tags, attributes, and byte content directly into a pre-sized
Vec without building an intermediate Node tree.

Made Encoder, EncodeNode, ByteWriter, and key write methods public
so crates outside wacore-binary can implement custom encoding.
… Vec)

Replace AttrsRef type alias (Vec) with a covariant enum that stores
0-1 attributes inline without heap allocation. Most WA protocol nodes
have 3-5 attributes so they still use the Many(Vec) variant, but
zero-attr and single-attr nodes (common in child elements) avoid the
Vec entirely.

The enum is covariant in the lifetime parameter (unlike SmallVec),
so it works with yoke::Yokeable for the zero-copy decode path.
Manual Yokeable impl uses transmute for lifetime erasure.

Implements: len, is_empty, iter, as_slice, push, with_capacity,
FromIterator, and serde::Serialize (via as_slice delegation).
@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a050128f-ab1f-49c8-9d88-cd914855b7cc

📥 Commits

Reviewing files that changed from the base of the PR and between 99d05c4 and 9e268ef.

📒 Files selected for processing (1)
  • wacore/src/iq/prekeys.rs

📝 Walkthrough

Walkthrough

Client gained a direct-binary IQ encode fast path and a centralized send-and-wait helper; encoder APIs and encoder module were made public; frame buffering generalized to support BytesMut; node attribute storage changed to an enum-backed AttrsRef; PreKey upload added direct encoder implementation and tests.

Changes

Cohort / File(s) Summary
Client request flow
src/request.rs
Added IqError::EncodeError(anyhow::Error); refactored Client::send_iq and added Client::send_and_wait_iq; Client::execute now attempts spec.encode_iq_direct fast path, maps encode/parse errors, and falls back to build_iq() when needed.
IqSpec & prekey direct encode
wacore/src/iq/spec.rs, wacore/src/iq/prekeys.rs
Added IqSpec::encode_iq_direct(&self, request_id, out) -> Result<bool, anyhow::Error> (default Ok(false)); PreKeyUploadSpec implements direct encoding via Encoder::new_vec and adds tests verifying byte-for-byte parity with marshal path.
Binary encoder public API
wacore/binary/src/encoder.rs, wacore/binary/src/lib.rs
Made encoder module public and promoted core types/methods from pub(crate)pub (ByteWriter, VecByteWriter, EncodeNode, Encoder, constructors and write helpers). Encoder::new_vec now clears the provided buffer before encoding.
Node attrs representation
wacore/binary/src/node.rs
Replaced AttrsRef<'a> alias with pub enum AttrsRef<'a> { Empty, Slice(Box<[ ... ]>) }; added from_vec, len, is_empty, as_slice, iter, FromIterator, Yokeable impl and adjusted serde wrapper serialization.
Frame buffer & socket send buffer
wacore/noise/src/framing.rs, src/socket/noise_socket.rs
Introduced pub trait FrameBuf with impls for Vec<u8> and BytesMut; encode_frame_into now accepts &mut impl FrameBuf. Noise socket switched outgoing buffer to BytesMut and uses zero-copy split().freeze() for frames.
Keepalive classification
src/keepalive.rs
classify_keepalive_error treats IqError::EncodeError(_) as KeepaliveResult::FatalFailure, making encode failures during keepalive fatal.
Decoder attr read optimization
wacore/binary/src/decoder.rs
read_attributes(size) returns AttrsRef::Empty for size == 0; non-zero path builds a Vec then converts via AttrsRef::from_vec.
Misc / Repo
.gitignore
Expanded ignore from dhat-heap.json to dhat-heap*.json.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Spec as IqSpec
    participant Encoder
    participant Socket
    participant Waiter as ResponseWaiter

    Note over Client,Waiter: Direct-encode IQ flow (new)

    Client->>Spec: encode_iq_direct(req_id, buf)
    alt Ok(true)
        Spec->>Encoder: Encoder::new_vec(buf)
        Encoder-->>Spec: writes encoded bytes
        Spec-->>Client: Ok(true)
        Client->>Waiter: register waiter keyed by req_id
        Client->>Socket: send pre-encoded bytes via send_and_wait_iq(timeout)
        alt send ok
            Waiter-->>Client: response / timeout / shutdown
            Client->>Spec: parse_response(node)
            Spec-->>Client: parsed Response or ParseError
        else send err
            Client->>Waiter: remove waiter
            Client-->>Client: return mapped IqError
        end
    else Ok(false) or Err
        Spec-->>Client: Ok(false) / Err
        Client->>Spec: build_iq() + marshal (fallback)
        Client->>Socket: send marshaled IQ via existing send path
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through buffers, bytes in line,
I taught the encoder to write in time,
Swapped Vec for BytesMut, split with cheer,
Sent pre-made IQs both far and near,
Empty or Slice — nibble, hop, hooray! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely and accurately captures all three main optimizations in the PR: zero-copy frame send, direct prekey encoding, and covariant AttrsRef, with specific performance metrics (-24.4% total, -43.6% blocks) demonstrating the improvements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/remaining-alloc-optimizations

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.

@github-actions

github-actions Bot commented Apr 15, 2026

Copy link
Copy Markdown

Benchmark Results

4 improvement(s):

Benchmark Current Baseline Change
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 34,946 38,500 -9.2%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 63,329 66,324 -4.5%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 63,400 66,390 -4.5%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 87,023 89,639 -2.9%
55 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 3,933 3,933 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 12,038 12,038 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 43,414 43,414 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 19,365 19,365 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 68,478 68,478 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 76,578 76,578 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 2,230 2,230 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 5,988 5,988 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 170,012 170,026 -0.0%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 191,750 191,742 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 875,950 875,949 +0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 966,961 966,963 -0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,453,903 1,453,944 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 2,585,125 2,585,393 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 9,422,890 9,423,273 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 44,696,309 44,522,913 +0.4%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 12,614,619 12,712,384 -0.8%
binary_benchmark::marshal_group::bench_marshal_allocating 71,234 71,236 -0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 71,287 71,289 -0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 98,354 98,357 -0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 78,788 78,791 -0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 71,334 71,336 -0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 7,527 7,517 +0.1%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 7,570 7,560 +0.1%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 9,282 9,273 +0.1%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 530,509 530,503 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 530,081 530,067 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 531,436 531,423 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 8,506,150 8,506,182 -0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 8,450,436 8,450,483 -0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 19,677,986 19,678,088 -0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 2,464 2,498 -1.4%
binary_benchmark::unpack_group::bench_unpack_uncompressed 787 787 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 526,732 526,732 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 4,986 5,028 -0.8%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 5,311 5,330 -0.4%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 5,343 5,357 -0.3%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 6,730 6,734 -0.1%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 477,570 477,570 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 11,574 11,605 -0.3%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 16,998,985 17,181,546 -1.1%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 157,923 157,923 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 5,511,084 5,511,084 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 158,737 158,737 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 296,767 296,767 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 707,098 707,030 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 12,460,056 12,346,249 +0.9%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 27,329,211 27,407,634 -0.3%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 3,467,011 3,467,011 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 125,771,973 126,084,643 -0.2%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,830,452 2,830,452 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 46,003 46,003 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 5,090,932 5,090,932 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 316,987 316,987 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 14,254,317 14,254,317 +0.0%

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

ℹ️ 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/binary/src/node.rs Outdated
Comment on lines +399 to +401
match n {
0 => Self::Empty,
_ => Self::Many(Vec::with_capacity(n)),

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 Preserve one-attribute fast path in AttrsRef::with_capacity

with_capacity(1) currently returns Many(Vec::with_capacity(1)), so callers that know there is exactly one attribute (notably Decoder::read_attributes(size)) still pay a heap allocation on the first push. That bypasses the new inline One variant for a common case (<count value="..."/>, similar single-attr nodes) and negates part of the intended allocation reduction from this commit. Initializing n == 1 as Empty (or otherwise routing first insert to One) keeps the single-attribute decode path allocation-free.

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.

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (3)
wacore/binary/src/encoder.rs (2)

621-628: ⚠️ Potential issue | 🟠 Major

Clear the Vec before starting a new encoded stanza.

new_vec() currently appends the leading format byte to whatever is already in buffer. That breaks the new reusable-buffer use case: calling it with a non-empty Vec<u8> will prefix stale bytes and produce a malformed frame.

Suggested fix
 impl<'v> Encoder<'static, VecByteWriter<'v>> {
     pub fn new_vec(buffer: &'v mut Vec<u8>) -> Result<Self> {
+        buffer.clear();
         let mut enc = Self {
             writer: VecByteWriter::new(buffer),
             string_hints: None,
         };
         enc.write_u8(0)?;
         Ok(enc)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/binary/src/encoder.rs` around lines 621 - 628, The Encoder::new_vec
currently appends the leading format byte to an existing buffer; modify
Encoder::new_vec so it clears the provided Vec<u8> before constructing the
VecByteWriter (or before writing the leading byte) to ensure the buffer is empty
for a new stanza; locate the new_vec function in impl<'v> Encoder<'static,
VecByteWriter<'v>> and call clear on the passed-in buffer (or ensure
VecByteWriter is initialized with an emptied buffer) prior to enc.write_u8(0).

912-920: ⚠️ Potential issue | 🟠 Major

Reject list lengths that exceed LIST_16 capacity.

This now-public API silently truncates len with len as u16 for values above 65535, which emits corrupt wire data instead of failing fast. Please return an error when the length is out of range.

Suggested fix
     pub fn write_list_start(&mut self, len: usize) -> Result<()> {
         if len == 0 {
             self.write_u8(token::LIST_EMPTY)?;
         } else if len < 256 {
             self.write_u8(248)?;
             self.write_u8(len as u8)?;
+        } else if u16::try_from(len).is_ok() {
+            let len = len as u16;
+            self.write_u8(249)?;
+            self.write_u16_be(len)?;
         } else {
-            self.write_u8(249)?;
-            self.write_u16_be(len as u16)?;
+            return Err(BinaryError::AttrParse(format!(
+                "list length exceeds LIST_16 max: {}",
+                len
+            )));
         }
         Ok(())
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/binary/src/encoder.rs` around lines 912 - 920, The write_list_start
function currently truncates oversized lengths when taking the LIST_16 path (it
casts len to u16 before calling write_u16_be), so update write_list_start to
explicitly reject lengths > 65535 by returning an error (Result::Err) instead of
silently truncating; in practice add a check before the else branch (or inside
the else branch for the 249 case) that if len > u16::MAX returns an appropriate
error (e.g., invalid input/length too large) so write_u16_be is only called with
a safe u16 conversion.
wacore/src/iq/prekeys.rs (1)

365-490: 🧹 Nitpick | 🔵 Trivial

Add a byte-for-byte equivalence test for the direct encoder.

This path now hand-writes the entire <iq> stanza, but the tests in this file still only cover build_iq() and response parsing. Please add a regression test that encode_iq_direct() produces the same bytes as build_iq() + marshal for the same PreKeyUploadSpec, so future protocol drift is caught locally instead of against the server.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/iq/prekeys.rs` around lines 365 - 490, Add a unit test that
constructs a representative PreKeyUploadSpec, then: 1) call
PreKeyUploadSpec::encode_iq_direct(request_id, &mut Vec) (which uses
PreKeyUploadIqNode) to produce bytes, and 2) call PreKeyUploadSpec::build_iq()
to get the InfoQuery/Node and marshal it with the existing node marshaller to
produce bytes; finally assert the two byte buffers are identical. Place the test
alongside the existing tests in this file, use the same request_id and spec data
for both paths, and fail on any mismatch to catch protocol drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wacore/binary/src/node.rs`:
- Around line 398-401: with_capacity currently maps n == 1 into
Many(Vec::with_capacity(1)) which forces a heap allocation and bypasses the
inline One variant; change AttrsRef::with_capacity so it explicitly returns the
One variant when n == 1 (construct the appropriate single-attribute placeholder
that One expects) and only return Many(Vec::with_capacity(n)) for n > 1, keeping
0 => Empty; update the with_capacity function and any constructors used by
AttrsRef::One to ensure the One variant is created correctly without allocating.

In `@wacore/src/iq/spec.rs`:
- Around line 14-24: The fast-path method encode_iq_direct currently only
returns a bool and drops InfoQuery.timeout so Client::execute cannot preserve
non-default timeouts; change the contract to surface timeout metadata (e.g.
change encode_iq_direct to return Result<(bool, Option<std::time::Duration>),
anyhow::Error> or accept a &mut Option<Duration> timeout out-parameter), update
all implementations to populate the timeout from InfoQuery.timeout when
returning Ok(true), and update Client::execute to consume that timeout instead
of falling back to the hard-coded 75s when the fast path is used; ensure
build_iq and any callers still work with the revised signature.

---

Outside diff comments:
In `@wacore/binary/src/encoder.rs`:
- Around line 621-628: The Encoder::new_vec currently appends the leading format
byte to an existing buffer; modify Encoder::new_vec so it clears the provided
Vec<u8> before constructing the VecByteWriter (or before writing the leading
byte) to ensure the buffer is empty for a new stanza; locate the new_vec
function in impl<'v> Encoder<'static, VecByteWriter<'v>> and call clear on the
passed-in buffer (or ensure VecByteWriter is initialized with an emptied buffer)
prior to enc.write_u8(0).
- Around line 912-920: The write_list_start function currently truncates
oversized lengths when taking the LIST_16 path (it casts len to u16 before
calling write_u16_be), so update write_list_start to explicitly reject lengths >
65535 by returning an error (Result::Err) instead of silently truncating; in
practice add a check before the else branch (or inside the else branch for the
249 case) that if len > u16::MAX returns an appropriate error (e.g., invalid
input/length too large) so write_u16_be is only called with a safe u16
conversion.

In `@wacore/src/iq/prekeys.rs`:
- Around line 365-490: Add a unit test that constructs a representative
PreKeyUploadSpec, then: 1) call PreKeyUploadSpec::encode_iq_direct(request_id,
&mut Vec) (which uses PreKeyUploadIqNode) to produce bytes, and 2) call
PreKeyUploadSpec::build_iq() to get the InfoQuery/Node and marshal it with the
existing node marshaller to produce bytes; finally assert the two byte buffers
are identical. Place the test alongside the existing tests in this file, use the
same request_id and spec data for both paths, and fail on any mismatch to catch
protocol drift.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba178aaf-a2df-4ea1-8e59-bf9eba48b2ea

📥 Commits

Reviewing files that changed from the base of the PR and between 2641743 and d830750.

📒 Files selected for processing (8)
  • src/request.rs
  • src/socket/noise_socket.rs
  • wacore/binary/src/encoder.rs
  • wacore/binary/src/lib.rs
  • wacore/binary/src/node.rs
  • wacore/noise/src/framing.rs
  • wacore/src/iq/prekeys.rs
  • wacore/src/iq/spec.rs

Comment thread wacore/binary/src/node.rs Outdated
Comment on lines +398 to +401
pub fn with_capacity(n: usize) -> Self {
match n {
0 => Self::Empty,
_ => Self::Many(Vec::with_capacity(n)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

with_capacity(1) still allocates and bypasses One.

decoder.read_attributes() builds through AttrsRef::with_capacity(size), so the size == 1 case takes this Many(Vec::with_capacity(1)) branch and never uses the inline One variant. That means the hot single-attribute decode path still heap-allocates, which defeats the main optimization this enum is supposed to provide.

Suggested fix
 pub fn with_capacity(n: usize) -> Self {
     match n {
-        0 => Self::Empty,
+        0 | 1 => Self::Empty,
         _ => Self::Many(Vec::with_capacity(n)),
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/binary/src/node.rs` around lines 398 - 401, with_capacity currently
maps n == 1 into Many(Vec::with_capacity(1)) which forces a heap allocation and
bypasses the inline One variant; change AttrsRef::with_capacity so it explicitly
returns the One variant when n == 1 (construct the appropriate single-attribute
placeholder that One expects) and only return Many(Vec::with_capacity(n)) for n
> 1, keeping 0 => Empty; update the with_capacity function and any constructors
used by AttrsRef::One to ensure the One variant is created correctly without
allocating.

Comment thread wacore/src/iq/spec.rs
Comment on lines +14 to +24
/// Optionally encode the IQ stanza directly into a pre-sized buffer,
/// bypassing the Node intermediate representation. Returns `true` if
/// the fast path was used; `false` falls back to `build_iq()` + marshal.
///
/// The buffer must contain the full binary-encoded `<iq>` stanza including
/// the leading format byte. `request_id` is the IQ request ID.
fn encode_iq_direct(
&self,
_request_id: &str,
_out: &mut Vec<u8>,
) -> Result<bool, anyhow::Error> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The fast-path API drops IQ timeout metadata.

build_iq() can carry a non-default InfoQuery.timeout, but encode_iq_direct() only hands back bytes. Once a spec returns Ok(true), Client::execute has no way to preserve that timeout and falls back to the hard-coded 75s path instead. Please surface timeout metadata through the fast-path contract as well, or the optimized path will change request behavior for those specs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/iq/spec.rs` around lines 14 - 24, The fast-path method
encode_iq_direct currently only returns a bool and drops InfoQuery.timeout so
Client::execute cannot preserve non-default timeouts; change the contract to
surface timeout metadata (e.g. change encode_iq_direct to return Result<(bool,
Option<std::time::Duration>), anyhow::Error> or accept a &mut Option<Duration>
timeout out-parameter), update all implementations to populate the timeout from
InfoQuery.timeout when returning Ok(true), and update Client::execute to consume
that timeout instead of falling back to the hard-coded 75s when the fast path is
used; ensure build_iq and any callers still work with the revised signature.

@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review this nitpicky check claims, DRY usage, unnecessary allocations, better options to the same solution, if it lacks in tests, if it can have something better to reuse (like if we want to reuse the same pattern for another).

@claude

claude Bot commented Apr 15, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 4m 22s —— View job


Review of PR #552

I've read every changed file and cross-referenced against the existing codebase. Here's a detailed review covering the specific areas you asked about.


1. Nitpicky Reviewer Claims — Verified

CodeRabbit: "Encoder::new_vec should clear the buffer"Not a real bug. Every call site passes a fresh Vec::new() or Vec::with_capacity(n) (see marshal.rs:45-47, marshal.rs:96-98, prekeys.rs:483-488). The existing marshal_to_vec also doesn't clear. Adding a .clear() would be defensive, but the claim that it "breaks the reusable-buffer use case" is speculative — no reusable-buffer caller exists today. Low priority, skip it.

CodeRabbit: "write_list_start silently truncates via len as u16"Valid but low-risk. The WhatsApp protocol never sends lists with >65535 items (prekey uploads cap at 812). But since the method is now pub, an external caller could hit this. A u16::try_from guard would be cheap insurance. Worth doing.

CodeRabbit: "encode_iq_direct drops timeout metadata"Not applicable to PreKeyUploadSpec. I checked: PreKeyUploadSpec::build_iq() returns InfoQuery::set(...) which has timeout: None. In fact, only KeepaliveSpec and RemoveCompanionDeviceSpec use custom timeouts across the entire codebase. So this is not a regression for the current fast-path spec. However, the trait contract is subtly wrong for future specs — if someone adds encode_iq_direct to a spec that uses a custom timeout, they'd silently lose it. Worth documenting in the trait doc or returning Option<Duration> alongside the bool.

Codex/CodeRabbit: "with_capacity(1) bypasses One"Valid and directly explains the benchmark regressions. The decoder calls AttrsRef::with_capacity(size) where size comes from the wire — single-attr nodes are extremely common (<count value="..."/>, <id>, <value>, etc.). with_capacity(1) goes to Many(Vec::with_capacity(1)), heap-allocating for the exact case the One variant was designed to optimize. Fix: 0 | 1 => Self::Empty so that .push() promotes Empty → One inline.


2. DRY Analysis

send_iq_raw is a near-clone of send_iq — This is the biggest DRY issue. send_iq_raw (request.rs:232-285) duplicates ~90% of send_iq (request.rs:129-196):

  • Same is_running double-check pattern
  • Same shutdown listener registration
  • Same response waiter insert/remove
  • Same futures::select! timeout/shutdown race
  • Same error mapping for ClientError

The only differences are: (a) send_iq_raw calls send_raw_bytes(buf) instead of send_node(node), and (b) it doesn't extract timeout from the query.

Suggestion: Extract a shared wait_for_iq_response(&self, req_id, timeout, send_fn) helper that takes a closure for the send step. This would eliminate the duplication and automatically fix the missing timeout propagation issue.

async fn send_and_wait_iq<F, Fut>(
    &self,
    req_id: String,
    timeout: Duration,
    send_fn: F,
) -> Result<Arc<wacore_binary::OwnedNodeRef>, IqError>
where
    F: FnOnce() -> Fut,
    Fut: Future<Output = Result<(), crate::client::ClientError>>,
{ /* shared logic */ }

3. Unnecessary Allocations

AttrsRef::with_capacity(1)Many(Vec) (see above): Heap allocates for single-attr nodes when One is available. This is the source of the +10-17% benchmark regressions on unmarshal/roundtrip paths. The Many variant's Vec header alone is 24 bytes on the stack + a heap pointer; One stores the pair inline.

PreKeyUploadIqNode::encode_attrs allocates a Jid every call:
At prekeys.rs:398: encoder.write_jid_owned(&Jid::new("", Server::Pn)). Jid::new heap-allocates the user string. Since this is always the same constant JID, consider a const/static or use write_jid_ref with a borrowed JidRef instead.

Vec::new() in execute fast path:
request.rs:216: let mut buf = Vec::new() is allocated even when encode_iq_direct returns Ok(false) (which is the default for all specs except PreKeyUploadSpec). This means every client.execute(spec) call for any spec now pays for a zero-capacity Vec allocation. Should either:

  • Move Vec::new() inside the fast-path check, or
  • Lazy-allocate: call encode_iq_direct with an option/flag first

4. Better Alternatives

FrameBuf trait vs bytes::BufMut: The FrameBuf trait (framing.rs:10-14) reinvents clear, reserve, and extend_from_slice — all of which BufMut already provides (and BytesMut already implements). Using BufMut + a clear call would avoid the custom trait entirely. Vec<u8> doesn't implement BufMut directly, but bytes provides it via extension. If you want to keep Vec<u8> support, the custom trait is fine, but worth noting the overlap.

EncodeNode for PreKeyUploadIqNode hand-encodes attr order: The comment at prekeys.rs:390 says "Attr order matches build_iq_node". This couples the fast path to an internal detail of RequestUtils::build_iq_node. If build_iq_node ever reorders attrs (e.g., alphabetizing), the direct encoder diverges silently. There's no test to catch this (see section 5).

Enum size consideration for AttrsRef: NodeStr<'a> is max(ptr+len, CompactString) = 24 bytes. ValueRef<'a> is similar. So the One variant stores a (NodeStr, ValueRef) tuple inline — that's ~48+ bytes. The Many variant stores Vec<...> = 24 bytes. This means size_of::<AttrsRef> is dominated by the One variant. Every AttrsRef enum pays the size of the largest variant even when Empty or Many. This inflates NodeRef size. Not a blocker, but worth checking with std::mem::size_of that the per-node overhead from the bigger enum is offset by the allocation savings.


5. Test Coverage Gaps

No equivalence test between encode_iq_direct and build_iq + marshal. This is the biggest gap. The direct encoder hand-writes the same wire format as the Node-based path. Any drift (attr order, byte encoding, etc.) will produce protocol errors that only surface against a real server. Adding a round-trip comparison test is critical:

#[test]
fn test_direct_encode_matches_build_iq() {
    let spec = PreKeyUploadSpec::new(/* ... */);
    let req_id = "test-123";

    // Fast path
    let mut direct_buf = Vec::new();
    spec.encode_iq_direct(req_id, &mut direct_buf).unwrap();

    // Slow path
    let iq = spec.build_iq();
    let request_utils = /* ... */;
    let node = request_utils.build_iq_node(iq, Some(req_id.to_string()));
    let marshal_buf = wacore_binary::marshal(&node).unwrap();

    assert_eq!(direct_buf, marshal_buf);
}

No FrameBuf test for BytesMut: The existing encode_frame_into tests only test with Vec<u8>. Add a parallel test with BytesMut to verify the trait impl works (especially split().freeze() behavior).

No test for AttrsRef::push transition from One to Many: The push logic has a std::mem::replace dance (node.rs:409). This path should be tested explicitly.

No unit test for AttrsRef::with_capacity correctness: Given it's the root cause of the benchmark regressions, this deserves a test verifying with_capacity(1) + push(item) doesn't allocate (once fixed).


6. Reusability — Can Another Spec Use This Pattern?

The encode_iq_direct pattern has a high adoption cost for new specs:

  1. You must implement EncodeNode manually, encoding every attr and child node by hand-calling write_string, write_list_start, write_bytes_with_len, etc.
  2. You must match the exact attr/content encoding order that build_iq_node would produce.
  3. There's no way to verify correctness short of a byte-comparison test (which doesn't exist yet).
  4. Timeout metadata is silently dropped.

For PreKeyUploadSpec, this tradeoff makes sense — it's the single hottest IQ (812 prekeys during registration) and called once per session. The 4,872 block savings is significant.

For most other specs, it's not worth it. The ~50 IQ specs in wacore/src/iq/ have 2-10 child nodes. The allocation savings from skipping the Node tree would be negligible for those. The maintenance cost of keeping two encoding paths in sync outweighs the benefit.

If you want reusability, consider a middle ground: a macro or builder that generates the EncodeNode impl from a declarative description (similar to how ProtocolNode derive works). This would let you opt into direct encoding without hand-writing wire bytes.


Summary of Actionable Items

Priority Issue Location
High with_capacity(1) bypasses One variant → benchmark regressions node.rs:398-401
High send_iq_raw is a near-clone of send_iq — extract shared helper request.rs:232-285
High No equivalence test for direct encode vs marshal path prekeys.rs
Medium Vec::new() allocated on every execute() call even when fast-path unused request.rs:216
Medium write_list_start should guard against len > u16::MAX encoder.rs:912-920
Low Constant JID allocation in PreKeyUploadIqNode::encode_attrs prekeys.rs:398
Low FrameBuf trait overlaps with bytes::BufMut framing.rs:10-14
Low Document timeout gap in encode_iq_direct trait contract spec.rs:14-26

|

- AttrsRef::with_capacity(1) now returns Empty instead of Many(Vec),
  fixing the benchmark regressions (+10-17% on unmarshal/roundtrip)
  caused by heap-allocating for single-attr nodes
- Encoder::new_vec clears buffer before writing format byte
- write_list_start rejects len > u16::MAX instead of truncating
- Vec::new() in execute() moved inside the fast-path block to avoid
  allocation when encode_iq_direct returns false (all non-prekey specs)
- Added unit test verifying encode_iq_direct produces identical bytes
  to build_iq + marshal

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/request.rs`:
- Line 244: The hardcoded default_timeout (Duration::from_secs(75)) reduces
flexibility for different IqSpec implementations; update the API so callers can
control timeout instead of using a fixed 75s. Specifically, modify
encode_iq_direct (or its return type) or add an optional trait method on IqSpec
to provide a per-spec timeout, then replace the direct use of default_timeout
with the provided timeout (falling back to a sensible default) and update
send_iq to accept or honor that timeout; reference symbols: default_timeout,
Duration::from_secs(75), encode_iq_direct, send_iq, IqSpec.
- Around line 233-288: The send_iq_raw implementation duplicates logic from
send_iq; extract a shared helper (e.g., handle_iq_request or
register_waiter_and_send) that encapsulates: checking is_running (use same
ordering semantics), registering/removing a oneshot waiter in response_waiters,
creating shutdown listener from shutdown_notifier, invoking send_raw_bytes (or
send_bytes closure passed in), running rt_timeout with the runtime, parsing
response via get_request_utils().parse_iq_response, and cleaning up the waiter
on timeout/shutdown/errors; then refactor send_iq_raw and send_iq to call this
helper with the appropriate request id, buffer/encode step, and error mapping so
the duplicated blocks (is_running checks, waiter registration/removal,
shutdown/timeout race, and response parsing) are centralized.

In `@wacore/binary/src/node.rs`:
- Around line 464-485: The unsafe Yokeable impl for AttrsRef (impl Yokeable for
AttrsRef<'static>) relies on AttrsRef remaining covariant in 'a; add a
compile-time covariance assertion to detect future regressions by introducing a
private phantom-check function that references AttrsRef with contravariant and
covariant contexts (e.g., a helper that accepts fn(AttrsRef<'static>) ->
AttrsRef<'_> or uses core::mem::transmute checks) and call it in the module so
the compiler will error if covariance is violated; update or document this check
alongside the unsafe methods make() and transform_mut() so future changes to
AttrsRef variants (Empty, One, Many) are caught early.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a015d5f0-49b6-498d-adf7-e9bf8f64dcc2

📥 Commits

Reviewing files that changed from the base of the PR and between d830750 and ee2de60.

📒 Files selected for processing (4)
  • src/request.rs
  • wacore/binary/src/encoder.rs
  • wacore/binary/src/node.rs
  • wacore/src/iq/prekeys.rs

Comment thread src/request.rs
Comment thread src/request.rs Outdated
return Err(IqError::NotConnected);
}

let default_timeout = Duration::from_secs(75);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Hardcoded timeout limits flexibility for future specs.

The 75-second default matches send_iq, but if future IqSpec implementations using encode_iq_direct need different timeouts, they'll be stuck with this value. Consider exposing an optional timeout in encode_iq_direct's return type or as a separate trait method if this becomes a practical concern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/request.rs` at line 244, The hardcoded default_timeout
(Duration::from_secs(75)) reduces flexibility for different IqSpec
implementations; update the API so callers can control timeout instead of using
a fixed 75s. Specifically, modify encode_iq_direct (or its return type) or add
an optional trait method on IqSpec to provide a per-spec timeout, then replace
the direct use of default_timeout with the provided timeout (falling back to a
sensible default) and update send_iq to accept or honor that timeout; reference
symbols: default_timeout, Duration::from_secs(75), encode_iq_direct, send_iq,
IqSpec.

Comment thread wacore/binary/src/node.rs
Comment on lines +464 to +485
unsafe impl<'a> yoke::Yokeable<'a> for AttrsRef<'static> {
type Output = AttrsRef<'a>;

fn transform(&'a self) -> &'a Self::Output {
self
}

fn transform_owned(self) -> Self::Output {
self
}

unsafe fn make(from: Self::Output) -> Self {
unsafe { std::mem::transmute(from) }
}

fn transform_mut<F>(&'a mut self, f: F)
where
F: 'static + for<'b> FnOnce(&'b mut Self::Output),
{
unsafe { f(std::mem::transmute::<&mut Self, &mut Self::Output>(self)) }
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unsafe Yokeable impl looks correct but warrants careful maintenance.

The safety argument relies on AttrsRef being covariant in 'a. This holds because:

  • Empty has no lifetime
  • One contains (NodeStr<'a>, ValueRef<'a>) which are both #[derive(Yokeable)]
  • Many(Vec<T>) is covariant in T

The transmute in make() and transform_mut() is safe given identical memory layout across lifetimes. Consider adding a compile-time covariance assertion (e.g., a phantom function) to catch accidental future changes that break covariance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/binary/src/node.rs` around lines 464 - 485, The unsafe Yokeable impl
for AttrsRef (impl Yokeable for AttrsRef<'static>) relies on AttrsRef remaining
covariant in 'a; add a compile-time covariance assertion to detect future
regressions by introducing a private phantom-check function that references
AttrsRef with contravariant and covariant contexts (e.g., a helper that accepts
fn(AttrsRef<'static>) -> AttrsRef<'_> or uses core::mem::transmute checks) and
call it in the module so the compiler will error if covariance is violated;
update or document this check alongside the unsafe methods make() and
transform_mut() so future changes to AttrsRef variants (Empty, One, Many) are
caught early.

- DRY: extract shared send_and_wait_iq() that both send_iq() and the
  direct-encode fast path use. Eliminates the duplicated send_iq_raw
  method (~55 lines of identical waiter/shutdown/timeout logic).
- Add compile-time covariance assertion for AttrsRef: if a future
  change makes AttrsRef invariant (breaking yoke::Yokeable safety),
  _assert_attrs_ref_covariant will fail to compile.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/request.rs`:
- Line 186: Trim the doc comment above the shared helper that “register[s]
waiter, send[s], race[s] response vs shutdown/timeout” to a one-line rationale
explaining why the helper exists (e.g., to centralize waiter registration and
consistent shutdown/timeout handling to avoid duplication and ensure uniform
cancellation semantics), removing the mechanical restatement of what it does;
update the comment that sits immediately above that helper in src/request.rs to
focus on this rationale.
- Around line 164-181: The code pre-generates req_id via generate_request_id()
and uses it on the direct-encode fast path, but on the fallback path it calls
spec.build_iq() which may leave the IQ id empty so send_iq() will generate a new
id; change execute() so after let iq = spec.build_iq(); if iq.id is empty/None
assign the previously created req_id (the one from generate_request_id()) to iq
before calling send_iq(), ensuring send_iq/send_and_wait_iq use the same request
id and avoiding the extra allocation; reference functions/fields:
generate_request_id, encode_iq_direct, build_iq, send_iq, send_and_wait_iq,
req_id.
- Around line 168-177: The current if let Ok(true) = spec.encode_iq_direct(...)
silently treats Err(_) as "not implemented" and falls back; replace that
conditional with a match on spec.encode_iq_direct(&req_id, &mut buf) so that
Ok(true) executes the fast-path (calling send_and_wait_iq/send_raw_bytes and
parse_response), Ok(false) continues to the existing marshal path, and Err(e) is
returned (or mapped) instead of being suppressed — e.g., return
Err(IqError::EncodeError(e)) (or propagate the underlying error) from the code
that currently calls encode_iq_direct in src/request.rs so failures in
encode_iq_direct are visible to client.execute().

In `@wacore/binary/src/node.rs`:
- Around line 390-395: The public enum AttrsRef with variants Empty/One/Many
allows callers to build Many(vec![]) or Many(vec![...]) making is_empty() and
derived PartialEq incorrect; change AttrsRef to a pub struct wrapper that
contains a private enum (or make the enum itself private) and expose controlled
constructors (e.g., AttrsRef::empty(), ::one(key,val), ::many(vec)) that
normalize inputs (convert empty vec -> Empty, single-element vec -> One) and
implement is_empty() and PartialEq manually (or derive on the normalized
internal state) so logically equivalent representations compare equal and empty
detection is correct; update any usages that construct AttrsRef directly to use
the new constructors (refer to AttrsRef, Empty/One/Many, is_empty, and
PartialEq).
- Around line 449-456: The FromIterator implementation for AttrsRef currently
always starts from AttrsRef::Empty and repeatedly grows, reintroducing
reallocations; fix by calling let mut iter = iter.into_iter(); use
iter.size_hint() to detect an exact/upper bound (e.g. let (_, high) =
iter.size_hint(); if let Some(sz) = high { result = AttrsRef::with_capacity(sz);
}) before the for loop, then push each item as before; this preserves
one-allocation behavior for exact-size iterators (see FromIterator for AttrsRef,
AttrsRef::Empty, result.push and Node::as_node_ref()).
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f64477a0-3d68-400f-8e90-3519024871f3

📥 Commits

Reviewing files that changed from the base of the PR and between 9be72ba and dddd0ee.

📒 Files selected for processing (2)
  • src/request.rs
  • wacore/binary/src/node.rs

Comment thread src/request.rs
Comment thread src/request.rs Outdated
Comment thread src/request.rs Outdated
Comment thread wacore/binary/src/node.rs Outdated
Comment thread wacore/binary/src/node.rs
- execute() now propagates Err from encode_iq_direct instead of
  silently falling back to build_iq
- Reuse the pre-generated req_id on the fallback path (set iq.id
  if empty) to avoid a redundant generate_request_id call
- FromIterator for AttrsRef uses size_hint to pre-allocate with
  with_capacity for exact-size iterators

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/request.rs`:
- Around line 169-180: The outbound encode failure from spec.encode_iq_direct is
incorrectly mapped to IqError::ParseError; change the Err(e) arm so encoding
failures are returned as a distinct error (e.g., IqError::EncodeError or
propagate the underlying error) instead of ParseError, and update the IqError
enum/type if necessary; keep the Ok(true) fast-path behavior (send_and_wait_iq
-> parse_response) unchanged and only alter the Err(e) branch to return the
proper encode-level error.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dffd0005-e632-499a-853a-9d13dd66ea59

📥 Commits

Reviewing files that changed from the base of the PR and between dddd0ee and 2cd04f0.

📒 Files selected for processing (2)
  • src/request.rs
  • wacore/binary/src/node.rs

Comment thread src/request.rs Outdated
…ession

The One variant stored a full (NodeStr, ValueRef) tuple inline (~72 bytes),
inflating every NodeRef by ~56 bytes and causing +9-17% instruction count
regressions on small-node benchmarks.

Replace with Empty | Slice(Box<[T]>): Box<[T]> is 16 bytes (ptr + len),
making the enum 24 bytes -- same as the original Vec<T>. Zero-attr nodes
still skip allocation via Empty. The decoder builds a Vec with exact
capacity then converts to boxed slice via into_boxed_slice().

Also fixes: clippy mut warning, propagates encode_iq_direct errors,
reuses req_id on fallback path.
Encoding failures from the direct-encode fast path were incorrectly
mapped to IqError::ParseError. Added distinct EncodeError variant
and classified it as FatalFailure in keepalive error handling.
Reserve capacity for the estimated wire size (812 prekeys * 40 bytes +
256 bytes overhead) before encoding. Avoids incremental Vec growth
during the encode pass. Marginal impact (~121 KB, -0.4%) since the
Vec growth strategy already amortizes well.

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

♻️ Duplicate comments (1)
wacore/binary/src/node.rs (1)

393-420: ⚠️ Potential issue | 🟠 Major

Normalize empty AttrsRef semantics or hide the variants.

Because AttrsRef is public, callers can still construct AttrsRef::Slice(Box::new([])). With derived PartialEq and is_empty() matching only Empty, that logically empty value reports non-empty and compares unequal to AttrsRef::Empty.

Suggested fix
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone)]
 pub enum AttrsRef<'a> {
     Empty,
     Slice(Box<[(NodeStr<'a>, ValueRef<'a>)]>),
 }
 
 impl<'a> AttrsRef<'a> {
@@
     #[inline]
     pub fn is_empty(&self) -> bool {
-        matches!(self, Self::Empty)
+        self.as_slice().is_empty()
     }
@@
 }
+
+impl PartialEq for AttrsRef<'_> {
+    #[inline]
+    fn eq(&self, other: &Self) -> bool {
+        self.as_slice() == other.as_slice()
+    }
+}

Stronger still: make the representation private so Empty remains the only zero-length form.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/binary/src/node.rs` around lines 393 - 420, The enum AttrsRef
currently exposes its variants (pub enum AttrsRef<'a>), letting callers create
Slice(Box::new([])) and breaking empty semantics; fix this by making the
representation private (e.g. change the enum visibility from pub enum
AttrsRef<'a> to pub(crate) enum AttrsRef<'a> or remove pub) so external code
cannot construct Slice directly, and keep public APIs (from_vec, len, is_empty)
as the only way to create/inspect AttrsRef; alternatively, if you must keep the
enum public, remove the derived PartialEq and implement a custom PartialEq and
adjust is_empty to treat AttrsRef::Slice(s) with s.len()==0 the same as
AttrsRef::Empty (ensure from_vec still converts empty Vec to Empty).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/keepalive.rs`:
- Around line 36-37: Add a unit test in the keepalive.rs tests that constructs
or simulates an IqError::EncodeError and asserts it maps to
KeepaliveResult::FatalFailure (matching the existing match arm for
IqError::EncodeError in the keepalive classification logic). Locate the matching
logic in keepalive.rs (the match handling IqError::InternalChannelClosed and
IqError::EncodeError) and write a test that invokes the classification path (or
calls the function that converts IqError to KeepaliveResult) to ensure
EncodeError is covered and asserts equality with KeepaliveResult::FatalFailure.

In `@src/request.rs`:
- Around line 169-185: Pre-allocate the encode buffer to avoid reallocations in
the fast path: replace the zero-capacity Vec::new() used before calling
spec.encode_iq_direct(&req_id, &mut buf) with a Vec::with_capacity(...) sized
for typical prekey IQs (e.g. 4 * 1024) or, if available, use a size hint from
the spec (e.g. spec.encoded_len_hint() or similar) to set capacity; keep the
rest of the logic around send_and_wait_iq(req_id, ... async {
self.send_raw_bytes(buf).await }) and parse_response(...) unchanged and still
map encode errors to IqError::EncodeError.

In `@wacore/binary/src/node.rs`:
- Around line 449-453: The safety comment for AttrsRef needs to be rewritten to
match the current enum representation (Empty | Slice(Box<[T]>)) and to justify
the transmute correctly: explain that Empty carries no lifetime, Slice contains
Box<[T]> which is covariant in T (and therefore in 'a), and thus AttrsRef is
covariant in 'a; update the comment above the transmute and the
_assert_attrs_ref_covariant function to refer to these exact variants and the
Box<[T]> covariance rather than the removed One/Many(Vec<T>) variants so the
unsafe justification matches the actual layout and invariants.

---

Duplicate comments:
In `@wacore/binary/src/node.rs`:
- Around line 393-420: The enum AttrsRef currently exposes its variants (pub
enum AttrsRef<'a>), letting callers create Slice(Box::new([])) and breaking
empty semantics; fix this by making the representation private (e.g. change the
enum visibility from pub enum AttrsRef<'a> to pub(crate) enum AttrsRef<'a> or
remove pub) so external code cannot construct Slice directly, and keep public
APIs (from_vec, len, is_empty) as the only way to create/inspect AttrsRef;
alternatively, if you must keep the enum public, remove the derived PartialEq
and implement a custom PartialEq and adjust is_empty to treat AttrsRef::Slice(s)
with s.len()==0 the same as AttrsRef::Empty (ensure from_vec still converts
empty Vec to Empty).
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 78860f4d-f168-4d65-a6df-f94078cd19a9

📥 Commits

Reviewing files that changed from the base of the PR and between dddd0ee and e466a67.

📒 Files selected for processing (4)
  • src/keepalive.rs
  • src/request.rs
  • wacore/binary/src/decoder.rs
  • wacore/binary/src/node.rs

Comment thread src/keepalive.rs
Comment on lines +36 to +37
| IqError::InternalChannelClosed
| IqError::EncodeError(_) => KeepaliveResult::FatalFailure,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Classification is correct, but add a unit test for EncodeError.

The fatal classification makes sense—encoding failures indicate a fundamental issue (e.g., malformed spec implementation), not a recoverable network condition. The tests cover all other IqError variants but miss EncodeError.

🧪 Suggested test
+    #[test]
+    fn test_classify_encode_error_is_fatal() {
+        assert_eq!(
+            classify_keepalive_error(&IqError::EncodeError(anyhow::anyhow!("encoding failed"))),
+            KeepaliveResult::FatalFailure,
+        );
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/keepalive.rs` around lines 36 - 37, Add a unit test in the keepalive.rs
tests that constructs or simulates an IqError::EncodeError and asserts it maps
to KeepaliveResult::FatalFailure (matching the existing match arm for
IqError::EncodeError in the keepalive classification logic). Locate the matching
logic in keepalive.rs (the match handling IqError::InternalChannelClosed and
IqError::EncodeError) and write a test that invokes the classification path (or
calls the function that converts IqError to KeepaliveResult) to ensure
EncodeError is covered and asserts equality with KeepaliveResult::FatalFailure.

Comment thread src/request.rs
Comment on lines +169 to +185
{
let mut buf = Vec::new();
match spec.encode_iq_direct(&req_id, &mut buf) {
Ok(true) => {
let response = self
.send_and_wait_iq(req_id, Duration::from_secs(75), async {
self.send_raw_bytes(buf).await
})
.await?;
return spec
.parse_response(response.get())
.map_err(IqError::ParseError);
}
Err(e) => return Err(IqError::EncodeError(e)),
Ok(false) => {}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider pre-allocating the buffer for the fast path.

Vec::new() starts at zero capacity and grows during encoding. Since prekey IQs are typically 1-4KB, a pre-sized buffer could avoid reallocations.

♻️ Optional optimization
-            let mut buf = Vec::new();
+            let mut buf = Vec::with_capacity(2048);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/request.rs` around lines 169 - 185, Pre-allocate the encode buffer to
avoid reallocations in the fast path: replace the zero-capacity Vec::new() used
before calling spec.encode_iq_direct(&req_id, &mut buf) with a
Vec::with_capacity(...) sized for typical prekey IQs (e.g. 4 * 1024) or, if
available, use a size hint from the spec (e.g. spec.encoded_len_hint() or
similar) to set capacity; keep the rest of the logic around
send_and_wait_iq(req_id, ... async { self.send_raw_bytes(buf).await }) and
parse_response(...) unchanged and still map encode errors to
IqError::EncodeError.

Comment thread wacore/binary/src/node.rs Outdated
@jlucaso1 jlucaso1 changed the title perf: zero-copy frame send, direct prekey encoding, covariant AttrsRef perf: zero-copy frame send, direct prekey encoding, covariant AttrsRef (-24.4% total, -43.6% blocks) Apr 15, 2026
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Apr 15, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 4m 6s —— View job


Review of PR #552

  • Read all changed files and understand the current state
  • Review zero-copy frame send (FrameBuf trait, noise_socket.rs)
  • Review direct binary encoding (encoder.rs, prekeys.rs, spec.rs)
  • Review covariant AttrsRef enum (node.rs, decoder.rs)
  • Review send_and_wait_iq helper and request flow (request.rs)
  • Review error handling (keepalive.rs, IqError::EncodeError)
  • Check for remaining issues after previous review rounds
  • Post final review

I've read every changed file and cross-referenced with the existing codebase. The PR has gone through multiple review rounds and many issues have been addressed. Here's what remains.

Overall Assessment

This is solid performance work. The send_and_wait_iq extraction, IqError::EncodeError variant, Encoder::new_vec clear, write_list_start bounds check, req_id reuse on fallback, and encode_iq_direct error propagation have all been addressed well. The AttrsRef design with Empty | Slice(Box<[T]>) is a clean simplification over the earlier One/Many approach and the covariance assertion + safety comment are correct.

Remaining Issues

1. No equivalence test for direct encode vs build_iq + marshal (High)

prekeys.rs:365-495 — The PreKeyUploadIqNode hand-writes the entire <iq> stanza byte-by-byte. If RequestUtils::build_iq_node ever reorders attributes or changes encoding, the two paths silently diverge, producing protocol errors that only surface against a real server. The attr order coupling is explicitly noted in a comment (prekeys.rs:390: "Attr order matches build_iq_node: id, xmlns, type, to") but there's no automated check.

A byte-comparison test that runs both paths with the same spec and asserts identical output would make this safe to maintain. This was flagged in the previous review and in CodeRabbit's review but hasn't been added yet.

2. AttrsRef::is_empty() and PartialEq can lie (Medium)

node.rs:419-421is_empty() uses matches!(self, Self::Empty), and PartialEq is derived. Since the enum variants are public, someone can construct AttrsRef::Slice(Box::new([])), which is logically empty but is_empty() returns false, and Empty != Slice(Box::new([])) under derived PartialEq.

Today from_vec normalizes empty vecs to Empty, so internal code is fine. But the public variants are a footgun. Two options:

  • Make the enum pub(crate) and wrap in a struct with controlled constructors
  • Or implement PartialEq manually via as_slice() and fix is_empty to delegate to as_slice().is_empty()

The second is lower-effort and sufficient.

3. Unnecessary Jid allocation in direct encoder (Low)

prekeys.rs:398encoder.write_jid_owned(&Jid::new("", Server::Pn)) heap-allocates a Jid on every call. Since this is a constant empty-user JID, you could use write_jid_ref with a stack-allocated JidRef instead:

encoder.write_jid_ref(&JidRef {
    user: NodeStr::Borrowed(""),
    server: Server::Pn,
    agent: 0,
    device: 0,
    integrator: 0,
})?;

This is minor since it's called once per registration, but it's ironic to have a heap allocation in a path designed to eliminate allocations.

Things That Look Good

  • send_and_wait_iq extraction (request.rs:197-252): Clean helper that eliminates the send_iq_raw duplication. The shutdown/timeout race, waiter registration, and cleanup logic is now centralized.

  • Error propagation (request.rs:171-184): The match on encode_iq_direct correctly returns IqError::EncodeError on failure instead of silently falling back. The IqError::EncodeError variant is properly classified as FatalFailure in keepalive.rs:37.

  • req_id reuse (request.rs:188-189): The fallback path now reuses the pre-generated req_id instead of generating a second one.

  • AttrsRef covariance (node.rs:443-447): The compile-time covariance assertion via _assert_attrs_ref_covariant is the right approach. The safety comment (node.rs:449-453) accurately describes the current Empty | Slice(Box<[T]>) layout.

  • Decoder path (decoder.rs:427-442): read_attributes correctly uses Vec::with_capacity(size) + from_vec(), getting exact-size allocation with normalization.

  • FromIterator (node.rs:437-441): The current from_vec(iter.into_iter().collect()) delegates to Vec's FromIterator which uses size_hint() for initial capacity, then normalizes. This is efficient.

  • Zero-copy frame send (noise_socket.rs:83-84, 148): BytesMut::split().freeze() retains the underlying allocation for the next frame. Clean approach.

  • Encoder::new_vec (encoder.rs:623): The buffer.clear() is in place. PreKeyUploadSpec::encode_iq_direct calls reserve() before new_vec(), and since clear() preserves capacity, the pre-sizing is effective.

  • write_list_start (encoder.rs:919-924): Now properly rejects len > u16::MAX with an error instead of silently truncating.


@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/keepalive.rs (1)

36-37: ⚠️ Potential issue | 🟡 Minor

Classification is correct; add unit test for coverage.

Treating EncodeError as fatal is appropriate—encoding failures indicate a fundamental implementation issue, not a transient network condition. However, the test suite covers all other IqError variants but misses EncodeError.

🧪 Suggested test
+    #[test]
+    fn test_classify_encode_error_is_fatal() {
+        assert_eq!(
+            classify_keepalive_error(&IqError::EncodeError(anyhow::anyhow!("encoding failed"))),
+            KeepaliveResult::FatalFailure,
+        );
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/keepalive.rs` around lines 36 - 37, Add a unit test in src/keepalive.rs
that exercises the match arm for IqError::EncodeError and asserts it maps to
KeepaliveResult::FatalFailure: create a dummy IqError::EncodeError instance,
call the function (or internal mapping) that contains the shown match (the code
handling IqError::InternalChannelClosed / IqError::EncodeError(_)), and assert
the result equals KeepaliveResult::FatalFailure; if the mapping helper is
private, place the test in the same module so it can access internals.
wacore/binary/src/node.rs (1)

393-421: ⚠️ Potential issue | 🟠 Major

Hide or normalize the public AttrsRef representation.

AttrsRef::Slice(Box::new([])) is still constructible outside this module, so a logically empty value can report false from is_empty() and compare unequal to AttrsRef::Empty because PartialEq is derived on the variant shape. Either make the representation private, or make is_empty()/PartialEq canonicalize through as_slice().

Minimal mitigation if you keep the enum public
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone)]
 pub enum AttrsRef<'a> {
     Empty,
     Slice(Box<[(NodeStr<'a>, ValueRef<'a>)]>),
 }
@@
     #[inline]
     pub fn is_empty(&self) -> bool {
-        matches!(self, Self::Empty)
+        self.as_slice().is_empty()
     }
 }
+
+impl PartialEq for AttrsRef<'_> {
+    fn eq(&self, other: &Self) -> bool {
+        self.as_slice() == other.as_slice()
+    }
+}

Run this to see whether AttrsRef variants are constructed or pattern-matched directly outside this module before deciding between a private representation and the minimal mitigation:

#!/bin/bash
rg -n -C2 --type=rust '\bAttrsRef::(Empty|Slice)\b'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/binary/src/node.rs` around lines 393 - 421, AttrsRef currently exposes
its variants so external code can construct AttrsRef::Slice(Box::new([])) which
breaks logical emptiness and PartialEq; fix by either making the representation
private (remove public visibility of the enum variants so only this module can
construct variants and keep pub AttrsRef type but not its variants) or by
canonicalizing comparisons and emptiness: add an as_slice(&self) ->
&[(NodeStr<'a>, ValueRef<'a>)] helper and change is_empty(&self) to check
as_slice().is_empty(), and replace the derived PartialEq with a manual impl
PartialEq for AttrsRef that compares via as_slice() (also ensure from_vec still
constructs Slice/Empty appropriately). Ensure references to
AttrsRef::Slice/Empty from outside are prevented if you choose the visibility
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wacore/src/iq/prekeys.rs`:
- Around line 426-441: The code currently calls
encoder.write_list_start(spec.pre_keys.len()) when encoding pre_keys; add a unit
test that constructs a Spec (or the struct used in this diff) with spec.pre_keys
= vec![] and asserts the encoded output equals build_iq's output for an empty
pre_keys case to ensure write_list_start(0) produces the expected binary; locate
the encoding path around write_list_start, write_string("list"),
write_string("key"), and write_string("id")/write_string("value") in the
functions handling prekey encoding (the block using spec.pre_keys) and add the
empty-list test exercising that exact branch.

---

Duplicate comments:
In `@src/keepalive.rs`:
- Around line 36-37: Add a unit test in src/keepalive.rs that exercises the
match arm for IqError::EncodeError and asserts it maps to
KeepaliveResult::FatalFailure: create a dummy IqError::EncodeError instance,
call the function (or internal mapping) that contains the shown match (the code
handling IqError::InternalChannelClosed / IqError::EncodeError(_)), and assert
the result equals KeepaliveResult::FatalFailure; if the mapping helper is
private, place the test in the same module so it can access internals.

In `@wacore/binary/src/node.rs`:
- Around line 393-421: AttrsRef currently exposes its variants so external code
can construct AttrsRef::Slice(Box::new([])) which breaks logical emptiness and
PartialEq; fix by either making the representation private (remove public
visibility of the enum variants so only this module can construct variants and
keep pub AttrsRef type but not its variants) or by canonicalizing comparisons
and emptiness: add an as_slice(&self) -> &[(NodeStr<'a>, ValueRef<'a>)] helper
and change is_empty(&self) to check as_slice().is_empty(), and replace the
derived PartialEq with a manual impl PartialEq for AttrsRef that compares via
as_slice() (also ensure from_vec still constructs Slice/Empty appropriately).
Ensure references to AttrsRef::Slice/Empty from outside are prevented if you
choose the visibility change.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ece36d00-1cd1-49b7-b751-9eaa34b29b99

📥 Commits

Reviewing files that changed from the base of the PR and between dddd0ee and cee2fe8.

📒 Files selected for processing (6)
  • .gitignore
  • src/keepalive.rs
  • src/request.rs
  • wacore/binary/src/decoder.rs
  • wacore/binary/src/node.rs
  • wacore/src/iq/prekeys.rs

Comment thread wacore/src/iq/prekeys.rs
Derived PartialEq treated Empty != Slice(Box::new([])) even though
both are logically empty. Manual PartialEq compares via as_slice()
so all representations of the same content are equal. is_empty()
now delegates to as_slice().is_empty() for consistency.
Verify direct encode matches build_iq + marshal for empty prekey
list (write_list_start(0)) and single prekey. Extracted shared
helper to avoid test duplication.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wacore/binary/src/node.rs`:
- Around line 388-389: The comment in node.rs that states "16 bytes" and "24
bytes" for Box<[T]>/Vec<T> is architecture-specific; update the comment near the
`Box<[T]>`/`Vec<T>` explanation (and any mention of `NodeRef`) to either prefix
those numeric sizes with "on 64-bit targets" or replace them with a relative
description (e.g., "smaller footprint than Vec<T> on typical 64-bit platforms")
so the rationale remains correct across architectures.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 138f3b29-95ea-4c79-a036-db850d9d8c89

📥 Commits

Reviewing files that changed from the base of the PR and between cee2fe8 and 99d05c4.

📒 Files selected for processing (1)
  • wacore/binary/src/node.rs

Comment thread wacore/binary/src/node.rs
Comment on lines +388 to +389
/// Uses `Box<[T]>` (16 bytes: ptr + len) instead of `Vec<T>` (24 bytes: ptr + len + cap)
/// or inline storage (which inflated NodeRef size). Zero-attr nodes skip allocation

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Qualify the size numbers as 64-bit-specific.

16 bytes and 24 bytes are only true on 64-bit targets. Either qualify this as “on 64-bit” or reword it as a relative size comparison so the rationale stays accurate across architectures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/binary/src/node.rs` around lines 388 - 389, The comment in node.rs
that states "16 bytes" and "24 bytes" for Box<[T]>/Vec<T> is
architecture-specific; update the comment near the `Box<[T]>`/`Vec<T>`
explanation (and any mention of `NodeRef`) to either prefix those numeric sizes
with "on 64-bit targets" or replace them with a relative description (e.g.,
"smaller footprint than Vec<T> on typical 64-bit platforms") so the rationale
remains correct across architectures.

@jlucaso1 jlucaso1 changed the title perf: zero-copy frame send, direct prekey encoding, covariant AttrsRef (-24.4% total, -43.6% blocks) perf: zero-copy frame send, direct prekey encoding, covariant AttrsRef Apr 15, 2026
@jlucaso1
jlucaso1 merged commit 0c506d5 into main Apr 15, 2026
11 of 12 checks passed
@jlucaso1
jlucaso1 deleted the perf/remaining-alloc-optimizations branch April 15, 2026 19:27

@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: 9e268ef861

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

// Zero-copy: split() moves the written data into a new BytesMut,
// freeze() converts it to Bytes. The original out_buf retains its
// allocated capacity for the next frame.
let frame = out_buf.split().freeze();

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 Preserve send buffer capacity across frame handoff

Using out_buf.split().freeze() here does not keep the reusable buffer capacity as intended: split() is split_to(len), which advances out_buf to the tail region. Under sustained traffic, each send reduces the remaining capacity by that frame size until it reaches zero, so encode_frame_into starts reallocating frequently. This turns the new hot path into allocation churn on long-lived connections and undermines the performance goal of this change.

Useful? React with 👍 / 👎.

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