Skip to content

Add 4 KiB logical block footprint tracking to stats - #1070

Open
seks99x wants to merge 1 commit into
RsyncProject:masterfrom
seks99x:updated-disk-blocks
Open

Add 4 KiB logical block footprint tracking to stats#1070
seks99x wants to merge 1 commit into
RsyncProject:masterfrom
seks99x:updated-disk-blocks

Conversation

@seks99x

@seks99x seks99x commented Sep 1, 2026

Copy link
Copy Markdown
Member

Fixes #1066
Original feature request by @birdie-github

Currently, rsync --stats reports Literal data, which represents the unmatched file-update data sent over the network. However, network payload is often completely disconnected from the actual disk I/O footprint on the receiving end. Depending on the transfer flags (e.g., default temp-file rebuilding vs. --inplace), a tiny network payload can result in massive disk writes, or vice versa. System administrators currently have no native visibility into the true I/O cost of a sync.

This PR introduces a new metric to the --stats output: Number of modified 4K blocks. It tracks exactly how many 4,096-byte boundaries are written to the receiver's disk, completely decoupling network delta payload from disk I/O footprint.
O(1) Zero-Overhead Implementation
To ensure tracking does not bottleneck the receiver's high-speed I/O loop, the block calculation operates entirely in O(1) time with zero system calls and no loop iterations.
Stateful Tracking: It uses a static state machine (last_tracked_fd and last_touched_blk) to remember the last logical block written. Because the receiver strictly writes sequentially, we can calculate block deltas using purely mathematical boundaries.
Math over Memory: For each write, it calculates the block boundaries using (offset % 4096) and len. It diffs this against the last known state and instantly updates the global counter.

Real-World Examples (Why this matters)
Here is a demonstration of how network data and disk I/O diverge, using a 4MB file where we inject a single byte at 10 different 4K block boundaries:

Case 1: Delta transfer with --inplace

# 1. Restore the base file to exactly match the scattered destination
cp dest_scattered.bin base.bin

# 2. Inject a single byte at 10 different 4K block boundaries
python3 -c 'f=open("base.bin", "r+b"); [(f.seek(i*4096), f.write(b"\xFF")) for i in range(1,11)]; f.close()'

# 3. Sync it
../rsync -a --stats --inplace -I --no-whole-file base.bin dest_scattered.bin | egrep "Literal data|touched"
Literal data: 20,480 bytes
Number of modified 4K blocks: 10

Context: Rsync sends a small amount of literal data over the network to patch the file. Because --inplace is used, the receiver seeks and updates only the 10 specific blocks on disk. Network and Disk I/O are both low.
Case 2: Delta transfer without --inplace (Default temp-file behavior)

# 1. Restore the base file to exactly match the scattered destination
cp dest_scattered.bin base.bin

# 2. Inject a single byte at 10 different 4K block boundaries
python3 -c 'f=open("base.bin", "r+b"); [(f.seek(i*4096), f.write(b"\xFF")) for i in range(1,11)]; f.close()'

# 3. Sync it
../rsync -a --stats -I --no-whole-file base.bin dest_scattered.bin | egrep "Literal data|touched" 

Literal data: 0 bytes
Number of modified 4K blocks: 1,024

Context: Rsync perfectly matches the unchanged data locally. The network payload is effectively zero. However, because rsync reconstructs the file into a new hidden temp file before moving it into place, the receiver's disk is forced to rewrite the entire 4MB file (1,024 blocks).

This adds a 'Number of modified 4K blocks' metric to the --stats output to decouple network delta payload from actual disk I/O. Previously, a small amount of literal data scattered across a file (especially with --inplace) could result in a massive number of disjointed disk writes with no visibility, and large sparse files masked their true physical storage footprint.

Technical details:
- Implemented a stateful block tracker in the receiver that calculates touched 4K boundaries using file offsets and lengths, including strict lseek awareness to accurately skip sparse file holes.
- Enforced strict per-file lifetime state with a reset hook inside receive_data(), successfully mitigating POSIX file descriptor (FD) recycling state leaks.
- Created MSG_BLOCK_STATS multiplex message to tunnel the block footprint safely out of the isolated receiver process and relay it over the network.
- Bumped PROTOCOL_VERSION to 33 and SUBPROTOCOL_VERSION to 8392 for safe PR testing.
- Added test suite covering contiguous, scattered, zero-byte, sparse file (hole-skipping), multi-file (FD reuse), batch mode, and maximum-I/O boundary conditions.
@seks99x
seks99x force-pushed the updated-disk-blocks branch 3 times, most recently from 8801271 to 59ef172 Compare September 2, 2026 17:26

@steadytao steadytao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks quite good to me overall, thank you. Note that CI failures are not because of your changes. Ubuntu is fixed on master, Cygwin will soon be the same so you can rebase then.

Comment thread fileio.c Outdated
if (len <= 0)
return;
extern struct stats stats;
static int last_tracked_fd = -1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File-descriptor numbers are reusable so a later file can receive the same descriptor and inherit last_touched_blk from the previous file. The counter then under-reports that files initial ranges. More generally, one “last block” cannot count unique blocks when writes revisit or move backwards. This needs per-file lifetime state and a representation capable of tracking non-monotonic ranges.

@seks99x seks99x Sep 2, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah totally missed that. I added a reset tracker function and hooked it in receive_data to ensure after every file the fd is reset. Added also an edge case for multiple files and another one for batch mode
Thanks for your review!

@seks99x
seks99x force-pushed the updated-disk-blocks branch 3 times, most recently from 811708d to ee67473 Compare September 2, 2026 23:36
@seks99x

seks99x commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

@steadytao ubuntu workflow is the only one forced running rsync with --protocol=30 which is failing the tests.

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.

Feature Request: Report unique 4 KiB logical blocks touched by rsync writes (e.g. in --stats)

2 participants