Skip to content

perf(chunk): hold ChunkPutRequest content as Bytes for zero-copy fan-out#1

Closed
jacderida wants to merge 1 commit into
mainfrom
perf/chunk-put-bytes
Closed

perf(chunk): hold ChunkPutRequest content as Bytes for zero-copy fan-out#1
jacderida wants to merge 1 commit into
mainfrom
perf/chunk-put-bytes

Conversation

@jacderida

Copy link
Copy Markdown
Owner

Summary

  • Replace ChunkPutRequest.content: Vec<u8> with content: bytes::Bytes.
  • Enable the bytes crate's serde feature so Bytes serialises identically to Vec<u8> on the wire (both become seq[u8] under postcard).
  • Update ChunkPutRequest::new / with_payment signatures and the two existing tests.

Why

A heaptrack capture against a release ant binary uploading a 20 MB file shows peak heap pinned to ~285 MB in alloc::raw_vec::RawVecInner::finish_grow, with the dominant call chain landing at ChunkMessage::encodechunk_put_to_close_group. The fan-out path at ant-client/ant-core/src/data/client/chunk.rs:173 does:

```rust
let request = ChunkPutRequest::with_payment(address, content.to_vec(), proof);
```

The content parameter is already a refcounted bytes::Bytes, but to_vec() deep-copies the entire 4 MB chunk into a fresh Vec<u8> — once per recipient. With CLOSE_GROUP_MAJORITY ≈ 5 peers and the AIMD store cap at 64, peak in-flight chunk-content can reach ~2.5 GB on a single client, which exceeds the 4 GB VM budget once DHT/QUIC/etc. overhead is added.

Switching content to Bytes removes the requirement for the caller to copy. The ant-client follow-up PR drops the to_vec() and passes the Bytes straight through, so each peer's spawned task shares a single 4 MB backing buffer via refcount instead of holding N independent copies.

Wire compatibility

Identical. Under postcard + serde, both Vec<u8> and bytes::Bytes are encoded as a varint length followed by the raw bytes. A mixed network of old and new clients/servers will interoperate.

Test plan

  • cargo check --all-features clean
  • cargo test --lib chunk — all 17 chunk tests pass (the two ChunkPutRequest tests were updated to construct Bytes::from_static(...))
  • Downstream consumer rebuild — ant-client PR coming next, which patches this branch in via [patch.crates-io] and drops the to_vec()

🤖 Generated with Claude Code

Replace `content: Vec<u8>` with `content: bytes::Bytes` on
ChunkPutRequest. Wire format is unchanged — Bytes serialises as a byte
sequence under postcard/serde, identical to Vec<u8> — 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) <noreply@anthropic.com>
@jacderida

Copy link
Copy Markdown
Owner Author

Wrong target — reopening against WithAutonomi/ant-protocol.

@jacderida jacderida closed this May 11, 2026
jacderida added a commit to jacderida/ant-client that referenced this pull request May 11, 2026
`chunk_put_with_proof` was deep-copying every chunk's content into a
fresh `Vec<u8>` via `Bytes::to_vec()` before stuffing it into a
`ChunkPutRequest`. Since `chunk_put_to_close_group` spawns one of these
per recipient (CLOSE_GROUP_MAJORITY ≈ 5) and the AIMD store controller
caps concurrent chunks at 64, peak in-flight chunk content reached
~64 × 5 × 4 MB = 2.5 GB just from these copies — enough to OOM-kill
`ant` on a 4 GB client VM partway through a 300 MB-or-larger upload.

Heaptrack against a clean-exit 20 MB upload (release `ant` on a 4 GB
VM) showed 285 MB peak in `alloc::raw_vec::RawVecInner::finish_grow`,
with 168 MB of that consumed via `ChunkMessage::encode` →
`chunk_put_to_close_group` — i.e. the per-recipient copy + encode loop.

ant-protocol now holds `ChunkPutRequest::content` as `bytes::Bytes`
(see jacderida/ant-protocol#1), so the caller
can pass the refcounted `Bytes` straight through. Each peer's spawned
task now shares the single 4 MB backing buffer instead of holding an
independent copy. Wire format is unchanged.

The patch.crates-io stanza pins ant-protocol to the perf branch commit
so the build resolves the Bytes-typed field. The pin should be removed
once a crates.io release containing the protocol change is published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jacderida added a commit to WithAutonomi/ant-client that referenced this pull request May 11, 2026
`chunk_put_with_proof` was deep-copying every chunk's content into a
fresh `Vec<u8>` via `Bytes::to_vec()` before stuffing it into a
`ChunkPutRequest`. Since `chunk_put_to_close_group` spawns one of these
per recipient (CLOSE_GROUP_MAJORITY ≈ 5) and the AIMD store controller
caps concurrent chunks at 64, peak in-flight chunk content reached
~64 × 5 × 4 MB = 2.5 GB just from these copies — enough to OOM-kill
`ant` on a 4 GB client VM partway through a 300 MB-or-larger upload.

Heaptrack against a clean-exit 20 MB upload (release `ant` on a 4 GB
VM) showed 285 MB peak in `alloc::raw_vec::RawVecInner::finish_grow`,
with 168 MB of that consumed via `ChunkMessage::encode` →
`chunk_put_to_close_group` — i.e. the per-recipient copy + encode loop.

ant-protocol now holds `ChunkPutRequest::content` as `bytes::Bytes`
(see jacderida/ant-protocol#1), so the caller
can pass the refcounted `Bytes` straight through. Each peer's spawned
task now shares the single 4 MB backing buffer instead of holding an
independent copy. Wire format is unchanged.

The patch.crates-io stanza pins ant-protocol to the perf branch commit
so the build resolves the Bytes-typed field. The pin should be removed
once a crates.io release containing the protocol change is published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jacderida added a commit to jacderida/ant-client that referenced this pull request May 13, 2026
`chunk_put_with_proof` was deep-copying every chunk's content into a
fresh `Vec<u8>` via `Bytes::to_vec()` before stuffing it into a
`ChunkPutRequest`. Since `chunk_put_to_close_group` spawns one of these
per recipient (CLOSE_GROUP_MAJORITY ≈ 5) and the AIMD store controller
caps concurrent chunks at 64, peak in-flight chunk content reached
~64 × 5 × 4 MB = 2.5 GB just from these copies — enough to OOM-kill
`ant` on a 4 GB client VM partway through a 300 MB-or-larger upload.

Heaptrack against a clean-exit 20 MB upload (release `ant` on a 4 GB
VM) showed 285 MB peak in `alloc::raw_vec::RawVecInner::finish_grow`,
with 168 MB of that consumed via `ChunkMessage::encode` →
`chunk_put_to_close_group` — i.e. the per-recipient copy + encode loop.

ant-protocol now holds `ChunkPutRequest::content` as `bytes::Bytes`
(see jacderida/ant-protocol#1), so the caller
can pass the refcounted `Bytes` straight through. Each peer's spawned
task now shares the single 4 MB backing buffer instead of holding an
independent copy. Wire format is unchanged.

The patch.crates-io stanza pins ant-protocol to the perf branch commit
so the build resolves the Bytes-typed field. The pin should be removed
once a crates.io release containing the protocol change is published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jacderida added a commit to jacderida/ant-client that referenced this pull request May 13, 2026
`chunk_put_with_proof` was deep-copying every chunk's content into a
fresh `Vec<u8>` via `Bytes::to_vec()` before stuffing it into a
`ChunkPutRequest`. Since `chunk_put_to_close_group` spawns one of these
per recipient (CLOSE_GROUP_MAJORITY ≈ 5) and the AIMD store controller
caps concurrent chunks at 64, peak in-flight chunk content reached
~64 × 5 × 4 MB = 2.5 GB just from these copies — enough to OOM-kill
`ant` on a 4 GB client VM partway through a 300 MB-or-larger upload.

Heaptrack against a clean-exit 20 MB upload (release `ant` on a 4 GB
VM) showed 285 MB peak in `alloc::raw_vec::RawVecInner::finish_grow`,
with 168 MB of that consumed via `ChunkMessage::encode` →
`chunk_put_to_close_group` — i.e. the per-recipient copy + encode loop.

ant-protocol now holds `ChunkPutRequest::content` as `bytes::Bytes`
(see jacderida/ant-protocol#1), so the caller
can pass the refcounted `Bytes` straight through. Each peer's spawned
task now shares the single 4 MB backing buffer instead of holding an
independent copy. Wire format is unchanged.

The patch.crates-io stanza pins ant-protocol to the perf branch commit
so the build resolves the Bytes-typed field. The pin should be removed
once a crates.io release containing the protocol change is published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jacderida added a commit to WithAutonomi/ant-client that referenced this pull request May 13, 2026
`chunk_put_with_proof` was deep-copying every chunk's content into a
fresh `Vec<u8>` via `Bytes::to_vec()` before stuffing it into a
`ChunkPutRequest`. Since `chunk_put_to_close_group` spawns one of these
per recipient (CLOSE_GROUP_MAJORITY ≈ 5) and the AIMD store controller
caps concurrent chunks at 64, peak in-flight chunk content reached
~64 × 5 × 4 MB = 2.5 GB just from these copies — enough to OOM-kill
`ant` on a 4 GB client VM partway through a 300 MB-or-larger upload.

Heaptrack against a clean-exit 20 MB upload (release `ant` on a 4 GB
VM) showed 285 MB peak in `alloc::raw_vec::RawVecInner::finish_grow`,
with 168 MB of that consumed via `ChunkMessage::encode` →
`chunk_put_to_close_group` — i.e. the per-recipient copy + encode loop.

ant-protocol now holds `ChunkPutRequest::content` as `bytes::Bytes`
(see jacderida/ant-protocol#1), so the caller
can pass the refcounted `Bytes` straight through. Each peer's spawned
task now shares the single 4 MB backing buffer instead of holding an
independent copy. Wire format is unchanged.

The patch.crates-io stanza pins ant-protocol to the perf branch commit
so the build resolves the Bytes-typed field. The pin should be removed
once a crates.io release containing the protocol change is published.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant