From f9bd72732efa2dfc1b102d42515db381a1ccd378 Mon Sep 17 00:00:00 2001 From: folbrich Date: Sat, 26 Sep 2026 19:43:15 +0200 Subject: [PATCH] Clone zeros in pieces as large as the largest chunk The null seed cloned runs of zeros from a file one block long, so every block of zeros written over existing data was its own clone and its own extent. Size the file of zeros to the largest chunk instead, rounded up to full blocks, and clone each run in pieces of that size. --- nullseed.go | 24 +++++++++++++++------ nullseed_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/nullseed.go b/nullseed.go index d1ceecb..37c6d99 100644 --- a/nullseed.go +++ b/nullseed.go @@ -11,6 +11,7 @@ import ( type nullChunkSeed struct { id ChunkID blockfile *os.File + blockLen uint64 canReflink bool } @@ -19,11 +20,17 @@ func newNullChunkSeed(dstFile string, blocksize uint64, max uint64) (*nullChunkS if err != nil { return nil, err } - var canReflink bool + var ( + canReflink bool + blockLen uint64 + ) if CanClone(dstFile, blockfile.Name()) { canReflink = true - b := make([]byte, blocksize) - if _, err := blockfile.Write(b); err != nil { + // Make the file of zeros as large as the largest chunk, rounded up to + // full blocks, so a run of zeros is cloned in few large pieces rather + // than one block at a time. + blockLen = (max + blocksize - 1) / blocksize * blocksize + if _, err := blockfile.Write(make([]byte, blockLen)); err != nil { return nil, err } } @@ -31,6 +38,7 @@ func newNullChunkSeed(dstFile string, blocksize uint64, max uint64) (*nullChunkS id: NewNullChunk(max).ID, canReflink: canReflink, blockfile: blockfile, + blockLen: blockLen, }, nil } @@ -69,6 +77,7 @@ func (s *nullChunkSeed) LongestMatchWith(chunks []IndexChunk) (int, SeedSegment) from: chunks[0].Start, to: chunks[n-1].Start + chunks[n-1].Size, blockfile: s.blockfile, + blockLen: s.blockLen, canReflink: s.canReflink, } } @@ -89,6 +98,7 @@ func (s *nullChunkSeed) IsInvalid() bool { type nullChunkSection struct { from, to uint64 blockfile *os.File + blockLen uint64 // Length of the blockfile, a multiple of the blocksize canReflink bool } @@ -155,15 +165,17 @@ func (s *nullChunkSection) clone(dst *os.File, offset, length, blocksize uint64) } copied += c2 - for blkOffset := dstAlignStart; blkOffset < dstAlignEnd; blkOffset += blocksize { - if err := cloneRange(dst, s.blockfile, 0, blocksize, blkOffset); err != nil { + for blkOffset := dstAlignStart; blkOffset < dstAlignEnd; { + n := min(s.blockLen, dstAlignEnd-blkOffset) + if err := cloneRange(dst, s.blockfile, 0, n, blkOffset); err != nil { // Not every filesystem that passes the CanClone probe can clone // every range. ZFS for example refuses to clone from the blockfile // before it has been committed to disk. Fall back to writing zeros. c3, _, err := s.copy(dst, blkOffset, dstAlignEnd-blkOffset) return copied + c3, cloned, err } - cloned += blocksize + cloned += n + blkOffset += n } return copied, cloned, nil } diff --git a/nullseed_test.go b/nullseed_test.go index eac8692..b5f6c0d 100644 --- a/nullseed_test.go +++ b/nullseed_test.go @@ -29,7 +29,7 @@ func TestNullChunkSectionCloneFallback(t *testing.T) { require.NoError(t, err) newSection := func() *nullChunkSection { - return &nullChunkSection{from: 0, to: length, blockfile: blockfile, canReflink: true} + return &nullChunkSection{from: 0, to: length, blockfile: blockfile, blockLen: blocksize, canReflink: true} } t.Run("copies zeros when cloning fails", func(t *testing.T) { @@ -92,7 +92,7 @@ func TestNullChunkSectionCloneFallback(t *testing.T) { require.NoError(t, err) defer dst.Close() - section := &nullChunkSection{from: from, to: from + sectionLen, blockfile: blockfile, canReflink: true} + section := &nullChunkSection{from: from, to: from + sectionLen, blockfile: blockfile, blockLen: blocksize, canReflink: true} copied, cloned, err := section.WriteInto(dst, from, sectionLen, bigBlock, false) require.NoError(t, err) assert.Equal(t, uint64(0), cloned) @@ -133,3 +133,54 @@ func TestNullChunkSectionCloneFallback(t *testing.T) { assert.Equal(t, make([]byte, length), got) }) } + +// A run of zeros is cloned in pieces as large as the blockfile, with the +// last piece shorter, rather than one block at a time. +func TestNullChunkSectionClonesLargePieces(t *testing.T) { + defer func() { cloneRange = CloneRange }() + + const ( + blocksize = 4096 + blockLen = 4 * blocksize + ) + dir := t.TempDir() + blockfile, err := os.CreateTemp(dir, ".tmp-block") + require.NoError(t, err) + defer blockfile.Close() + + type call struct{ srcOffset, length, dstOffset uint64 } + var calls []call + cloneRange = func(dst, src *os.File, srcOffset, srcLength, dstOffset uint64) error { + calls = append(calls, call{srcOffset, srcLength, dstOffset}) + // Write the zeros a real clone would have produced + _, err := dst.WriteAt(make([]byte, srcLength), int64(dstOffset)) + return err + } + + // Starts 100 bytes into the first block and covers 9 full blocks after it + from := uint64(100) + length := uint64(blocksize - from + 9*blocksize + 50) + fileLen := from + length + 1000 + dstName := filepath.Join(dir, "out") + require.NoError(t, os.WriteFile(dstName, bytes.Repeat([]byte{0xff}, int(fileLen)), 0644)) + dst, err := os.OpenFile(dstName, os.O_RDWR, 0) + require.NoError(t, err) + defer dst.Close() + + section := &nullChunkSection{from: from, to: from + length, blockfile: blockfile, blockLen: blockLen, canReflink: true} + copied, cloned, err := section.WriteInto(dst, from, length, blocksize, false) + require.NoError(t, err) + assert.Equal(t, uint64(9*blocksize), cloned) + assert.Equal(t, length-9*blocksize, copied) + assert.Equal(t, []call{ + {0, blockLen, blocksize}, + {0, blockLen, 5 * blocksize}, + {0, blocksize, 9 * blocksize}, + }, calls) + + got, err := os.ReadFile(dstName) + require.NoError(t, err) + assert.Equal(t, bytes.Repeat([]byte{0xff}, int(from)), got[:from]) + assert.Equal(t, make([]byte, length), got[from:from+length]) + assert.Equal(t, bytes.Repeat([]byte{0xff}, int(fileLen-from-length)), got[from+length:]) +}