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:]) +}