-
-
Notifications
You must be signed in to change notification settings - Fork 125
perf: zero-copy frame send, direct prekey encoding, covariant AttrsRef #552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
6878d77
42a9f5a
d830750
ee2de60
9be72ba
dddd0ee
2cd04f0
5b7c6b2
e466a67
1611d62
edf8a6c
6eee41f
cee2fe8
99d05c4
9e268ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,4 +10,4 @@ docs | |
| .claude | ||
| __pycache__ | ||
| .codex | ||
| dhat-heap.json | ||
| dhat-heap*.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,8 @@ pub enum IqError { | |
| ServerError { code: u16, text: String }, | ||
| #[error("Internal channel closed unexpectedly")] | ||
| InternalChannelClosed, | ||
| #[error("Failed to encode IQ request: {0}")] | ||
| EncodeError(anyhow::Error), | ||
| #[error("Failed to parse IQ response: {0}")] | ||
| ParseError(#[from] anyhow::Error), | ||
| } | ||
|
|
@@ -130,39 +132,95 @@ impl Client { | |
| &self, | ||
| query: InfoQuery<'_>, | ||
| ) -> Result<Arc<wacore_binary::OwnedNodeRef>, IqError> { | ||
| // Fail fast if the client is shutting down | ||
| if !self.is_running.load(Ordering::Relaxed) { | ||
| return Err(IqError::NotConnected); | ||
| } | ||
|
|
||
| let default_timeout = Duration::from_secs(75); | ||
| let iq_timeout = query.timeout.unwrap_or(default_timeout); | ||
| let req_id = query | ||
| .id | ||
| .clone() | ||
| .unwrap_or_else(|| self.generate_request_id()); | ||
|
|
||
| let request_utils = self.get_request_utils(); | ||
| let node = request_utils.build_iq_node(query, Some(req_id.clone())); | ||
|
|
||
| self.send_and_wait_iq(req_id, iq_timeout, async { self.send_node(node).await }) | ||
| .await | ||
| } | ||
|
|
||
| /// Executes an IQ specification and returns the typed response. | ||
| /// | ||
| /// This is a convenience method that combines building the IQ request, | ||
| /// sending it, and parsing the response into a single operation. | ||
| /// | ||
| /// # Example | ||
| /// | ||
| /// ```ignore | ||
| /// use wacore::iq::groups::GroupQueryIq; | ||
| /// | ||
| /// let group_info = client.execute(GroupQueryIq::new(&group_jid)).await?; | ||
| /// println!("Group subject: {}", group_info.subject); | ||
| /// ``` | ||
| pub async fn execute<S>(&self, spec: S) -> Result<S::Response, IqError> | ||
| where | ||
| S: wacore::iq::spec::IqSpec, | ||
| { | ||
| let req_id = self.generate_request_id(); | ||
|
|
||
| // Direct-encode fast path: skip Node tree for hot IQ specs (e.g. PreKeyUploadSpec) | ||
| { | ||
| 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) => {} | ||
| } | ||
| } | ||
|
Comment on lines
+169
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider pre-allocating the buffer for the fast path.
♻️ Optional optimization- let mut buf = Vec::new();
+ let mut buf = Vec::with_capacity(2048);🤖 Prompt for AI Agents |
||
|
|
||
| let mut iq = spec.build_iq(); | ||
| if iq.id.is_none() { | ||
| iq.id = Some(req_id); | ||
| } | ||
| let response = self.send_iq(iq).await?; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| spec.parse_response(response.get()) | ||
| .map_err(IqError::ParseError) | ||
| } | ||
|
|
||
| /// Centralizes waiter registration and shutdown/timeout handling. | ||
| async fn send_and_wait_iq<F>( | ||
| &self, | ||
| req_id: String, | ||
| timeout: Duration, | ||
| send_fn: F, | ||
| ) -> Result<Arc<wacore_binary::OwnedNodeRef>, IqError> | ||
| where | ||
| F: std::future::Future<Output = Result<(), crate::client::ClientError>>, | ||
| { | ||
| if !self.is_running.load(Ordering::Relaxed) { | ||
| return Err(IqError::NotConnected); | ||
| } | ||
|
|
||
| let (tx, rx) = futures::channel::oneshot::channel(); | ||
| self.response_waiters | ||
| .lock() | ||
| .await | ||
| .insert(req_id.clone(), tx); | ||
|
|
||
| let request_utils = self.get_request_utils(); | ||
| let node = request_utils.build_iq_node(query, Some(req_id.clone())); | ||
|
|
||
| // Register the shutdown listener BEFORE sending to avoid a window where | ||
| // a shutdown fires between send_node() completing and listen() being called. | ||
| let shutdown = self.shutdown_notifier.listen(); | ||
|
|
||
| // Re-check after registering the listener to close the race window where | ||
| // shutdown fires between the initial check and the listen() call above. | ||
| if !self.is_running.load(Ordering::Acquire) { | ||
| self.response_waiters.lock().await.remove(&req_id); | ||
| return Err(IqError::NotConnected); | ||
| } | ||
|
|
||
| if let Err(e) = self.send_node(node).await { | ||
| if let Err(e) = send_fn.await { | ||
| self.response_waiters.lock().await.remove(&req_id); | ||
| return match e { | ||
| crate::client::ClientError::Socket(s_err) => Err(IqError::Socket(s_err)), | ||
|
|
@@ -171,11 +229,9 @@ impl Client { | |
| }; | ||
| } | ||
|
|
||
| // Race the IQ response against shutdown so we fail fast on disconnect | ||
| // instead of waiting the full timeout. | ||
|
|
||
| let request_utils = self.get_request_utils(); | ||
| futures::select! { | ||
| result = rt_timeout(&*self.runtime, iq_timeout, rx).fuse() => { | ||
| result = rt_timeout(&*self.runtime, timeout, rx).fuse() => { | ||
| match result { | ||
| Ok(Ok(response_node)) => match request_utils.parse_iq_response(response_node.get()) { | ||
| Ok(()) => Ok(response_node), | ||
|
|
@@ -194,27 +250,4 @@ impl Client { | |
| } | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| /// Executes an IQ specification and returns the typed response. | ||
| /// | ||
| /// This is a convenience method that combines building the IQ request, | ||
| /// sending it, and parsing the response into a single operation. | ||
| /// | ||
| /// # Example | ||
| /// | ||
| /// ```ignore | ||
| /// use wacore::iq::groups::GroupQueryIq; | ||
| /// | ||
| /// let group_info = client.execute(GroupQueryIq::new(&group_jid)).await?; | ||
| /// println!("Group subject: {}", group_info.subject); | ||
| /// ``` | ||
| pub async fn execute<S>(&self, spec: S) -> Result<S::Response, IqError> | ||
| where | ||
| S: wacore::iq::spec::IqSpec, | ||
| { | ||
| let iq = spec.build_iq(); | ||
| let response = self.send_iq(iq).await?; | ||
| spec.parse_response(response.get()) | ||
| .map_err(IqError::ParseError) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,9 +78,10 @@ impl NoiseSocket { | |
| send_job_rx: async_channel::Receiver<SendJob>, | ||
| ) { | ||
| let mut write_counter: u32 = 0; | ||
| // Reusable buffers -- capacity stays allocated between sends | ||
| let mut enc_buf = Vec::with_capacity(4096); | ||
| let mut out_buf = Vec::with_capacity(4096); | ||
| // BytesMut: split().freeze() yields a zero-copy Bytes while retaining | ||
| // the underlying allocation for the next frame. | ||
| let mut out_buf = BytesMut::with_capacity(4096); | ||
|
|
||
| while let Ok(job) = send_job_rx.recv().await { | ||
| let result = Self::process_send_job( | ||
|
|
@@ -106,24 +107,21 @@ impl NoiseSocket { | |
| write_counter: &mut u32, | ||
| plaintext: &[u8], | ||
| enc_buf: &mut Vec<u8>, | ||
| out_buf: &mut Vec<u8>, | ||
| out_buf: &mut BytesMut, | ||
| ) -> SendResult { | ||
| let counter = *write_counter; | ||
|
|
||
| if plaintext.len() <= INLINE_ENCRYPT_THRESHOLD { | ||
| // Copy into reusable enc_buf, encrypt in place | ||
| enc_buf.clear(); | ||
| enc_buf.extend_from_slice(plaintext); | ||
| if let Err(e) = write_key.encrypt_in_place_with_counter(counter, enc_buf) { | ||
| return Err(EncryptSendError::crypto(anyhow::anyhow!(e.to_string()))); | ||
| } | ||
|
|
||
| out_buf.clear(); | ||
| if let Err(e) = wacore::framing::encode_frame_into(enc_buf, None, out_buf) { | ||
| return Err(EncryptSendError::framing(e)); | ||
| } | ||
| } else { | ||
| // Large messages: encrypt on blocking thread (reads plaintext, returns new ciphertext) | ||
| let write_key = write_key.clone(); | ||
| let plaintext_owned = plaintext.to_vec(); | ||
|
|
||
|
|
@@ -139,15 +137,15 @@ impl NoiseSocket { | |
| } | ||
| }; | ||
|
|
||
| out_buf.clear(); | ||
| if let Err(e) = wacore::framing::encode_frame_into(&ciphertext, None, out_buf) { | ||
| return Err(EncryptSendError::framing(e)); | ||
| } | ||
| } | ||
|
|
||
| // copy_from_slice so out_buf retains its capacity for the next send | ||
| let frame = bytes::Bytes::copy_from_slice(out_buf); | ||
| out_buf.clear(); | ||
| // 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(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Using Useful? React with 👍 / 👎. |
||
| if let Err(e) = transport.send(frame).await { | ||
| return Err(EncryptSendError::transport(e)); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
IqErrorvariants but missEncodeError.🧪 Suggested test
🤖 Prompt for AI Agents