Skip to content

perf(chunk): share chunk Bytes across close-group fan-out (drop to_vec)#82

Open
jacderida wants to merge 1 commit into
WithAutonomi:mainfrom
jacderida:perf/chunk-put-bytes
Open

perf(chunk): share chunk Bytes across close-group fan-out (drop to_vec)#82
jacderida wants to merge 1 commit into
WithAutonomi:mainfrom
jacderida:perf/chunk-put-bytes

Conversation

@jacderida
Copy link
Copy Markdown
Contributor

Summary

Why this matters

A heaptrack capture of a release ant binary uploading a 20 MB file on a 4 GB client VM (DEV-01 testnet) shows peak heap ≈ 285 MB pinned to alloc::raw_vec::RawVecInner::finish_grow, with 168 MB consumed via ChunkMessage::encodechunk_put_to_close_group. Trace summary:

peak heap memory consumption: 570.98 MB    (for a 20 MB file)
total memory leaked: 22 MB                  ← clean exit, no real leak
total runtime: 53 s

Source of the bloat: chunk_put_to_close_group spawns CLOSE_GROUP_MAJORITY ≈ 5 per-peer tasks via FuturesUnordered, each calling chunk_put_with_proof, which until now did:

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

Even though content arrives as a refcounted bytes::Bytes, that .to_vec() deep-copies the whole 4 MB chunk into a fresh Vec<u8> — once per recipient. Combined with the AIMD store concurrency cap at 64 (raised from the prior static 8 in commit 768e3bd in late April), peak in-flight chunk content reaches:

64 chunks × 5 peers × 4 MB-copy × 2 (copy + encode buffer) ≈ 2.5 GB

which is exactly the gap between the 256 MB peak the spilling design intended (file.rs:130 comment) and the actual ~3.55 GB anon-rss observed at every OOM kill on the DEV-01 1 GB / 500 MB / 300 MB uploaders. Lowering the cap would also restore 4 GB-VM viability, but this PR addresses the root cause: the per-recipient copy.

With this change, each peer's spawned task holds the same single 4 MB backing buffer via Bytes' refcount — N copies → 1 copy per chunk.

Wire compatibility

Unchanged. Bytes and Vec<u8> serialise identically under postcard/serde (varint length + raw bytes). Mixed networks of patched/unpatched clients and servers interoperate.

Patch.crates-io stanza

This PR adds:

[patch.crates-io]
ant-protocol = { git = \"https://github.com/jacderida/ant-protocol.git\", rev = \"4aac7d3...\" }

The pin is against the perf branch on the jacderida fork; the ant-protocol PR is at WithAutonomi/ant-protocol#6. The stanza should be removed once that PR lands and a crates.io release containing it is published.

Test plan

  • cargo check --workspace — clean
  • cargo check --bin ant — clean (the deployable binary builds)
  • heaptrack-derived reasoning verified against captured .zst profiles
  • Deploy: cherry-pick onto the DEV-01 testnet branch, build via saorsa-deploy build ant, re-provision a 1 GB / 500 MB / 300 MB uploader on a 4 GB VM, confirm OOMs cease and memory stays well below the 3.55 GB ceiling

Known follow-up

cargo test --workspace currently fails on a transitive dev-dep build of ant-node 0.11.1, which reads request.content as Vec<u8> in storage/handler.rs:239. The deployable ant binary doesn't link ant-node, so this doesn't block the operational fix, but ant-node will need a matching update when its protocol pin advances — out of scope for this PR.

🤖 Generated with Claude Code

`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