Review caught a release-blocking regression in the previous commit: with
--inplace --sparse, interior zero runs inside *matching* blocks were left
allocated.
The scan I added applied only to the write path. The use_seek branch --
reached via skip_matched() when an in-place update finds identical data --
still trimmed just the leading and trailing zeros and lseek()'d over the whole
middle. Before the change, write_file() fed that data through
SPARSE_WRITE_SIZE slices, so an all-zero slice in the middle of a large
matching block became a deferred hole like any other; afterwards those blocks
stayed fully allocated. Reproduced with the reported case: an 8 MiB file whose
every 32 KiB block is 4 KiB data / 24 KiB zeros / 4 KiB data, copied onto an
identical destination with --inplace --sparse --no-whole-file
--block-size=32768, occupied 2048 KiB before this series, 8192 KiB after the
previous commit, and 2048 KiB again with this one. Content was byte-identical
throughout; only the on-disk sparseness regressed.
Rather than duplicate the scan in the use_seek branch, drop that branch and run
both cases through the one loop, with the sole difference factored into
emit_sparse_span(): a span that is not becoming a hole is written normally, or
merely seeked past when the bytes on disk already match. The hole itself is
flushed by the existing flush_sparse_hole(), which already picks do_punch_hole()
over do_lseek() while inside the preallocated extent -- and for an in-place
transfer the receiver sets preallocated_len to the basis size, so a matched
interior hole is genuinely deallocated rather than skipped over.
Verified by differential fuzzing against the pre-series binary: 37 file shapes
(including the reported one, runs either side of the SPARSE_WRITE_SIZE
threshold, all-zero and hole-free files, and randomised mixes) across both the
plain and --inplace modes, comparing contents and allocated blocks. Contents
match and allocation is never worse than before the series.
(cherry picked from commit 231de22f7c)
(cherry picked from commit bb804f80f6)
write_file()'s sparse path sliced each span into SPARSE_WRITE_SIZE (1024-byte)
pieces and write_sparse() issued one write() syscall per slice. Copying a
large *non-sparse* file with --sparse therefore cost roughly one write() per
kilobyte -- about a million write() calls for a 1 GiB file -- which on real
storage ran far slower than the same copy without --sparse (the bug report
measured 1.36 MB/s vs 391 MB/s, ~280x). The 1024-byte chunk is also smaller
than a filesystem block, so it cannot even create finer holes than a plain
copy could.
Rewrite write_sparse() to scan the whole span itself: it looks for interior
runs of zeros that are at least SPARSE_WRITE_SIZE long -- the same hole
granularity rsync has always used -- and emits each intervening non-zero
region (which may include shorter zero runs not worth a hole) with a single
write(). do_punch_hole() advances the file offset just like the lseek() path,
so flushing a deferred hole between segments keeps the position correct.
The hole granularity is unchanged, so sparseness is identical; only the
syscall pattern changes. Measured on a 100 MiB random (hole-free) file:
write() syscalls drop from 100,730 to 6,125 (~16x), now tracking the data's
natural chunking rather than its size in kilobytes. Verified byte-identical
and equally sparse output for hole-free, large-hole, small-interior-hole,
all-zero, --inplace, and --preallocate cases.
testsuite/sparse-write-count_test.py copies a 16 MiB hole-free file under
strace and asserts the write() count stays far below the old size/1024
behaviour (it skips where strace is unavailable).
(cherry picked from commit 5ecab683a8)
(cherry picked from commit 6a15079cab)
- All the memory-allocation macros now auto-check for failure and exit
with a failure message that incudes the caller's file and lineno
info. This includes strdup().
- Added the `--max-alloc=SIZE` option to be able to override the memory
allocator's sanity-check limit. It defaults to 1G (as before).
Fixes bugzilla bug 12769.
- Make "len" parameter of do_punch_hole an OFF_T.
- Clear sparse_past_write in sparse_end(), otherwise when write_sparse()
is called for the next file, do_punch_hole() will be called with a pos
that's not actually the current position in file, causing it to fail.
The new code tries to punch holes in the destination file using newer
Linux fallocate features. It also supports a --whole-file + --sparse +
--inplace copy on any filesystem by truncating the destination file.
Fix a problem where sparse_seek could get left non-zero when we
did not finish writing all the data that would take us to that
sparse gap. Issue pointed out by David Taylor.
- Standardized the format of the opening comment, including adding a
brief description of what's in the file for those that lacked it.
- Added some missing copyright lines.
- Some minor whitespace tweaks (in a few of the files).
"read_size".
- Changed some int vars in map_ptr() needed to be int32 vars.
- All callers expect map_ptr() to return back the full "len" bytes,
so we never shorten this value, even near EOF--the read will
just get zero-padded, as needed.
- Die with an error if someone calls map_ptr() with a len < 0, or
if read_size is computed as a value < 0.
a (possibly approximate) window size and an optional block size
(which is used to round-up the window size if it is non-zero).
- Don't set window_start behind the supplied offset in map_ptr().