Skip to content

perf(storage): don't lose vectored writes through wrapping and boxing - #654

Open
zond wants to merge 1 commit into
ikatson:mainfrom
zond:storage-forward-vectored-writes
Open

zond wants to merge 1 commit into
ikatson:mainfrom
zond:storage-forward-vectored-writes

Conversation

@zond

@zond zond commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

pwrite_all_vectored() is the write path the storage module's own header argues for: one positioned write call for a chunk that isn't contiguous in memory. The peer's read buffer is a ringbuffer, so a Piece message's payload comes out of it as two slices whenever it wrapped around the end, and file_ops::write_chunk hands the pair to the storage as it is - on unix the filesystem storage turns that into a single pwritev, with nothing to join first. That is every chunk whose bytes happened to wrap, not some boundary case in the torrent: a chunk lives inside one piece and never straddles two, and one that spans a file boundary is already one vectored call per file.

It has a default that splits the call into two pwrite_all()s, there for storages that can't do better - and every wrapper around a storage took that default instead of passing the call on, so the storage underneath never saw a vectored write.

Box is the one that matters most, because Box is what a torrent holds: anything it doesn't forward, no storage in rqbit is ever asked. The three middlewares are the same shape.

Forward it in all four. The timing middleware logs it as the single operation it is rather than as two writes; the slow one sleeps once, not twice, for a write the storage does once; the cache copies both halves into the piece under one lock and one LRU lookup - where taking them one at a time cost two of each - and hands the write on whole, which is what factoring the cache update out of pwrite_all() is for. Each wrapper is checked against a probe that records the vectored writes that arrive as one call, so one that drops back to the default fails the test.

Yet another PR produced by my helpful Claude Fable.

pwrite_all_vectored() is the write path the storage module's own header argues
for: one positioned write call for a chunk that isn't contiguous in memory. The
peer's read buffer is a ringbuffer, so a Piece message's payload comes out of it
as two slices whenever it wrapped around the end, and file_ops::write_chunk
hands the pair to the storage as it is - on unix the filesystem storage turns
that into a single pwritev, with nothing to join first. That is every chunk
whose bytes happened to wrap, not some boundary case in the torrent: a chunk
lives inside one piece and never straddles two, and one that spans a file
boundary is already one vectored call per file.

It has a default that splits the call into two pwrite_all()s, there for storages
that can't do better - and every wrapper around a storage took that default
instead of passing the call on, so the storage underneath never saw a vectored
write.

Box<U> is the one that matters most, because Box<dyn TorrentStorage> is what a
torrent holds: anything it doesn't forward, no storage in rqbit is ever asked.
The three middlewares are the same shape.

Forward it in all four. The timing middleware logs it as the single operation it
is rather than as two writes; the slow one sleeps once, not twice, for a write
the storage does once; the cache copies both halves into the piece under one
lock and one LRU lookup - where taking them one at a time cost two of each - and
hands the write on whole, which is what factoring the cache update out of
pwrite_all() is for. Each wrapper is checked against a probe that records the
vectored writes that arrive as one call, so one that drops back to the default
fails the test.

Co-Authored-By: Claude Opus 5 <[email protected]>
@zond
zond force-pushed the storage-forward-vectored-writes branch from 0b25bad to 5e266b0 Compare September 7, 2026 13:46
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