From 4aac7d35b69a5d0906a6f2acd5d1d0beb52dcbb8 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Mon, 11 May 2026 22:57:41 +0100 Subject: [PATCH] perf(chunk): hold ChunkPutRequest content as Bytes for zero-copy fan-out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `content: Vec` with `content: bytes::Bytes` on ChunkPutRequest. Wire format is unchanged — Bytes serialises as a byte sequence under postcard/serde, identical to Vec — but the in-memory representation is now refcounted, so callers that send the same chunk to multiple peers (notably close-group replication) share a single backing buffer instead of deep-copying the 4 MB payload per peer. Heaptrack against a 20 MB upload on a release ant binary showed the client's peak heap dominated by RawVecInner::finish_grow calls in ChunkPutRequest construction, with 168 MB consumed via ChunkMessage::encode → chunk_put_to_close_group. The fan-out path was running `content.to_vec()` once per recipient at chunk.rs:173, which the new Bytes-typed field eliminates from the caller side. This commit is the protocol-side half of the fix; the ant-client side drops the `to_vec()` in a follow-up PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.toml | 2 +- src/chunk.rs | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 510e822..3390d67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ postcard = { version = "1.1.3", features = ["use-std"] } rmp-serde = "1" # Byte utilities -bytes = "1" +bytes = { version = "1", features = ["serde"] } hex = "0.4" # Logging (optional — behind `logging` feature flag, mirroring ant-node) diff --git a/src/chunk.rs b/src/chunk.rs index d2d838d..351b574 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -6,6 +6,7 @@ //! This module defines the wire protocol messages for chunk operations //! using postcard serialization for compact, fast encoding. +use bytes::Bytes; use serde::{Deserialize, Serialize}; /// Protocol identifier for chunk operations. @@ -121,12 +122,18 @@ impl ChunkMessage { // ============================================================================= /// Request to store a chunk. +/// +/// `content` is held as `bytes::Bytes` so that callers fanning the same +/// chunk out to multiple recipients (e.g. close-group replication) share a +/// single backing buffer via refcount instead of deep-copying the 4 MB +/// payload per peer. Wire format is unchanged: `Bytes` serializes as a +/// byte sequence, identical to `Vec` under postcard/serde. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChunkPutRequest { /// The content-addressed identifier (BLAKE3 of content). pub address: XorName, /// The chunk data. - pub content: Vec, + pub content: Bytes, /// Optional payment proof (serialized `ProofOfPayment`). /// Required for new chunks unless already verified. pub payment_proof: Option>, @@ -135,7 +142,7 @@ pub struct ChunkPutRequest { impl ChunkPutRequest { /// Create a new PUT request. #[must_use] - pub fn new(address: XorName, content: Vec) -> Self { + pub fn new(address: XorName, content: Bytes) -> Self { Self { address, content, @@ -145,7 +152,7 @@ impl ChunkPutRequest { /// Create a new PUT request with payment proof. #[must_use] - pub fn with_payment(address: XorName, content: Vec, payment_proof: Vec) -> Self { + pub fn with_payment(address: XorName, content: Bytes, payment_proof: Vec) -> Self { Self { address, content, @@ -386,7 +393,7 @@ mod tests { #[test] fn test_put_request_encode_decode() { let address = [0xAB; 32]; - let content = vec![1, 2, 3, 4, 5]; + let content = Bytes::from_static(&[1, 2, 3, 4, 5]); let request = ChunkPutRequest::new(address, content.clone()); let msg = ChunkMessage { request_id: 42, @@ -409,7 +416,7 @@ mod tests { #[test] fn test_put_request_with_payment() { let address = [0xAB; 32]; - let content = vec![1, 2, 3, 4, 5]; + let content = Bytes::from_static(&[1, 2, 3, 4, 5]); let payment = vec![10, 20, 30]; let request = ChunkPutRequest::with_payment(address, content.clone(), payment.clone());