diff options
| author | Jakob Unterwurzacher | 2016-07-01 23:29:31 +0200 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2016-07-01 23:32:27 +0200 | 
| commit | f2b4d57068d13b6dc3de2ccc6550675d11d34cfa (patch) | |
| tree | b90d31b0375e41318f2ce879df14f36bff4e3b5e | |
| parent | ae77d1852793c4dd43c8ddd69de574223aecce5e (diff) | |
fusefrontend: coalesce grows in Truncate()
We were growing the file block-by-block which was pretty
inefficient. We now coalesce all the grows into a single
Ftruncate. Also simplifies the code!
Simplistic benchmark: Before:
  $ time truncate -s 1000M foo
  real	0m0.568s
After:
  $ time truncate -s 1000M foo
  real	0m0.205s
| -rw-r--r-- | internal/contentenc/offsets.go | 1 | ||||
| -rw-r--r-- | internal/fusefrontend/file.go | 48 | ||||
| -rw-r--r-- | internal/fusefrontend/file_holes.go | 4 | ||||
| -rw-r--r-- | tests/matrix/matrix_test.go | 27 | 
4 files changed, 52 insertions, 28 deletions
| diff --git a/internal/contentenc/offsets.go b/internal/contentenc/offsets.go index da0551a..256ea26 100644 --- a/internal/contentenc/offsets.go +++ b/internal/contentenc/offsets.go @@ -66,6 +66,7 @@ func (be *ContentEnc) PlainSizeToCipherSize(plainSize uint64) uint64 {  }  // Split a plaintext byte range into (possibly partial) blocks +// Returns an empty slice if length == 0.  func (be *ContentEnc) ExplodePlainRange(offset uint64, length uint64) []intraBlock {  	var blocks []intraBlock  	var nextBlock intraBlock diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 513dbbf..8f13553 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -217,6 +217,8 @@ const FALLOC_FL_KEEP_SIZE = 0x01  //  // Called by Write() for normal writing,  // and by Truncate() to rewrite the last file block. +// +// Empty writes do nothing and are allowed.  func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) {  	// Read header from disk, create a new one if the file is empty @@ -282,6 +284,8 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) {  }  // Write - FUSE call +// +// If the write creates a hole, pads the file to the next block boundary.  func (f *file) Write(data []byte, off int64) (uint32, fuse.Status) {  	f.fdLock.RLock()  	defer f.fdLock.RUnlock() @@ -362,7 +366,6 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  	wlock.lock(f.ino)  	defer wlock.unlock(f.ino)  	var err error -  	// Common case first: Truncate to zero  	if newSize == 0 {  		err = syscall.Ftruncate(int(f.fd.Fd()), 0) @@ -374,7 +377,6 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  		f.header = nil  		return fuse.OK  	} -  	// We need the old file size to determine if we are growing or shrinking  	// the file  	fi, err := f.fd.Stat() @@ -388,12 +390,10 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  		newB := float32(newSize) / float32(f.contentEnc.PlainBS())  		tlog.Debug.Printf("ino%d: FUSE Truncate from %.2f to %.2f blocks (%d to %d bytes)", f.ino, oldB, newB, oldSize, newSize)  	} -  	// File size stays the same - nothing to do  	if newSize == oldSize {  		return fuse.OK  	} -  	// File grows  	if newSize > oldSize {  		// File was empty, create new header @@ -405,30 +405,22 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  		}  		// New blocks to add  		addBlocks := f.contentEnc.ExplodePlainRange(oldSize, newSize-oldSize) -		for _, b := range addBlocks { -			// First and last block may be partial and must be actually written -			if b.IsPartial() { -				off, _ := b.PlaintextRange() -				off += b.Skip -				_, status := f.doWrite(make([]byte, b.Length), int64(off)) -				if status != fuse.OK { -					return status -				} -			} else { -				// Complete all-zero blocks can stay all-zero because we do file -				// hole passthrough. -				// TODO We are growing the file block-by-block which is pretty -				// inefficient. We could coalesce all the grows into a single -				// Ftruncate. -				off, length := b.CiphertextRange() -				err = syscall.Ftruncate(int(f.fd.Fd()), int64(off+length)) -				if err != nil { -					tlog.Warn.Printf("grow Ftruncate returned error: %v", err) -					return fuse.ToStatus(err) -				} +		if len(addBlocks) >= 2 { +			f.zeroPad(oldSize) +		} +		lastBlock := addBlocks[len(addBlocks)-1] +		if lastBlock.IsPartial() { +			off, _ := lastBlock.PlaintextRange() +			_, status := f.doWrite(make([]byte, lastBlock.Length), int64(off+lastBlock.Skip)) +			return status +		} else { +			off, length := lastBlock.CiphertextRange() +			err = syscall.Ftruncate(f.intFd(), int64(off+length)) +			if err != nil { +				tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err)  			} +			return fuse.ToStatus(err)  		} -		return fuse.OK  	} else {  		// File shrinks  		blockNo := f.contentEnc.PlainOffToBlockNo(newSize) @@ -440,14 +432,14 @@ func (f *file) Truncate(newSize uint64) fuse.Status {  			var status fuse.Status  			data, status = f.doRead(plainOff, lastBlockLen)  			if status != fuse.OK { -				tlog.Warn.Printf("shrink doRead returned error: %v", err) +				tlog.Warn.Printf("Truncate: shrink doRead returned error: %v", err)  				return status  			}  		}  		// Truncate down to the last complete block  		err = syscall.Ftruncate(int(f.fd.Fd()), int64(cipherOff))  		if err != nil { -			tlog.Warn.Printf("shrink Ftruncate returned error: %v", err) +			tlog.Warn.Printf("Truncate: shrink Ftruncate returned error: %v", err)  			return fuse.ToStatus(err)  		}  		// Append partial block diff --git a/internal/fusefrontend/file_holes.go b/internal/fusefrontend/file_holes.go index 46a5865..1d9a5fb 100644 --- a/internal/fusefrontend/file_holes.go +++ b/internal/fusefrontend/file_holes.go @@ -22,6 +22,10 @@ func (f *file) createsHole(plainSize uint64, off int64) bool {  func (f *file) zeroPad(plainSize uint64) fuse.Status {  	lastBlockLen := plainSize % f.contentEnc.PlainBS()  	missing := f.contentEnc.PlainBS() - lastBlockLen +	if missing == 0 { +		// Already block-aligned +		return fuse.OK +	}  	pad := make([]byte, missing)  	tlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing)  	_, status := f.doWrite(pad, int64(plainSize)) diff --git a/tests/matrix/matrix_test.go b/tests/matrix/matrix_test.go index eb49d59..d59a677 100644 --- a/tests/matrix/matrix_test.go +++ b/tests/matrix/matrix_test.go @@ -113,6 +113,8 @@ func TestWrite100x100(t *testing.T) {  	}  } +// Hint for calculating reference md5sums: +// dd if=/dev/zero count=1 bs=XYZ | md5sum  func TestTruncate(t *testing.T) {  	fn := test_helpers.DefaultPlainDir + "/truncate"  	file, err := os.Create(fn) @@ -143,6 +145,31 @@ func TestTruncate(t *testing.T) {  	if test_helpers.Md5fn(fn) != "620f0b67a91f7f74151bc5be745b7110" {  		t.Errorf("wrong content")  	} +	// Truncate to zero +	file.Truncate(0) +	test_helpers.VerifySize(t, fn, 0) +	// Grow to 10MB (creates file holes) +	var sz int +	sz = 10 * 1024 * 1024 +	file.Truncate(int64(sz)) +	test_helpers.VerifySize(t, fn, sz) +	if test_helpers.Md5fn(fn) != "f1c9645dbc14efddc7d8a322685f26eb" { +		t.Errorf("wrong content") +	} +	// Grow to 10MB + 100B (partial block on the end) +	sz = 10*1024*1024 + 100 +	file.Truncate(int64(sz)) +	test_helpers.VerifySize(t, fn, sz) +	if test_helpers.Md5fn(fn) != "c23ea79b857b91a7ff07c6ecf185f1ca" { +		t.Errorf("wrong content") +	} +	// Grow to 20MB (creates file holes, partial block on the front) +	sz = 20 * 1024 * 1024 +	file.Truncate(int64(sz)) +	test_helpers.VerifySize(t, fn, sz) +	if test_helpers.Md5fn(fn) != "8f4e33f3dc3e414ff94e5fb6905cba8c" { +		t.Errorf("wrong content") +	}  }  func TestAppend(t *testing.T) { | 
