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
This commit is contained in:
parent
ae77d18527
commit
f2b4d57068
@ -66,6 +66,7 @@ func (be *ContentEnc) PlainSizeToCipherSize(plainSize uint64) uint64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Split a plaintext byte range into (possibly partial) blocks
|
// 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 {
|
func (be *ContentEnc) ExplodePlainRange(offset uint64, length uint64) []intraBlock {
|
||||||
var blocks []intraBlock
|
var blocks []intraBlock
|
||||||
var nextBlock intraBlock
|
var nextBlock intraBlock
|
||||||
|
@ -217,6 +217,8 @@ const FALLOC_FL_KEEP_SIZE = 0x01
|
|||||||
//
|
//
|
||||||
// Called by Write() for normal writing,
|
// Called by Write() for normal writing,
|
||||||
// and by Truncate() to rewrite the last file block.
|
// 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) {
|
func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) {
|
||||||
|
|
||||||
// Read header from disk, create a new one if the file is empty
|
// 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
|
// 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) {
|
func (f *file) Write(data []byte, off int64) (uint32, fuse.Status) {
|
||||||
f.fdLock.RLock()
|
f.fdLock.RLock()
|
||||||
defer f.fdLock.RUnlock()
|
defer f.fdLock.RUnlock()
|
||||||
@ -362,7 +366,6 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
|
|||||||
wlock.lock(f.ino)
|
wlock.lock(f.ino)
|
||||||
defer wlock.unlock(f.ino)
|
defer wlock.unlock(f.ino)
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Common case first: Truncate to zero
|
// Common case first: Truncate to zero
|
||||||
if newSize == 0 {
|
if newSize == 0 {
|
||||||
err = syscall.Ftruncate(int(f.fd.Fd()), 0)
|
err = syscall.Ftruncate(int(f.fd.Fd()), 0)
|
||||||
@ -374,7 +377,6 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
|
|||||||
f.header = nil
|
f.header = nil
|
||||||
return fuse.OK
|
return fuse.OK
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need the old file size to determine if we are growing or shrinking
|
// We need the old file size to determine if we are growing or shrinking
|
||||||
// the file
|
// the file
|
||||||
fi, err := f.fd.Stat()
|
fi, err := f.fd.Stat()
|
||||||
@ -388,12 +390,10 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
|
|||||||
newB := float32(newSize) / float32(f.contentEnc.PlainBS())
|
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)
|
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
|
// File size stays the same - nothing to do
|
||||||
if newSize == oldSize {
|
if newSize == oldSize {
|
||||||
return fuse.OK
|
return fuse.OK
|
||||||
}
|
}
|
||||||
|
|
||||||
// File grows
|
// File grows
|
||||||
if newSize > oldSize {
|
if newSize > oldSize {
|
||||||
// File was empty, create new header
|
// File was empty, create new header
|
||||||
@ -405,30 +405,22 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
|
|||||||
}
|
}
|
||||||
// New blocks to add
|
// New blocks to add
|
||||||
addBlocks := f.contentEnc.ExplodePlainRange(oldSize, newSize-oldSize)
|
addBlocks := f.contentEnc.ExplodePlainRange(oldSize, newSize-oldSize)
|
||||||
for _, b := range addBlocks {
|
if len(addBlocks) >= 2 {
|
||||||
// First and last block may be partial and must be actually written
|
f.zeroPad(oldSize)
|
||||||
if b.IsPartial() {
|
}
|
||||||
off, _ := b.PlaintextRange()
|
lastBlock := addBlocks[len(addBlocks)-1]
|
||||||
off += b.Skip
|
if lastBlock.IsPartial() {
|
||||||
_, status := f.doWrite(make([]byte, b.Length), int64(off))
|
off, _ := lastBlock.PlaintextRange()
|
||||||
if status != fuse.OK {
|
_, status := f.doWrite(make([]byte, lastBlock.Length), int64(off+lastBlock.Skip))
|
||||||
return status
|
return status
|
||||||
}
|
} else {
|
||||||
} else {
|
off, length := lastBlock.CiphertextRange()
|
||||||
// Complete all-zero blocks can stay all-zero because we do file
|
err = syscall.Ftruncate(f.intFd(), int64(off+length))
|
||||||
// hole passthrough.
|
if err != nil {
|
||||||
// TODO We are growing the file block-by-block which is pretty
|
tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err)
|
||||||
// inefficient. We could coalesce all the grows into a single
|
}
|
||||||
// Ftruncate.
|
return fuse.ToStatus(err)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return fuse.OK
|
|
||||||
} else {
|
} else {
|
||||||
// File shrinks
|
// File shrinks
|
||||||
blockNo := f.contentEnc.PlainOffToBlockNo(newSize)
|
blockNo := f.contentEnc.PlainOffToBlockNo(newSize)
|
||||||
@ -440,14 +432,14 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
|
|||||||
var status fuse.Status
|
var status fuse.Status
|
||||||
data, status = f.doRead(plainOff, lastBlockLen)
|
data, status = f.doRead(plainOff, lastBlockLen)
|
||||||
if status != fuse.OK {
|
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
|
return status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Truncate down to the last complete block
|
// Truncate down to the last complete block
|
||||||
err = syscall.Ftruncate(int(f.fd.Fd()), int64(cipherOff))
|
err = syscall.Ftruncate(int(f.fd.Fd()), int64(cipherOff))
|
||||||
if err != nil {
|
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)
|
return fuse.ToStatus(err)
|
||||||
}
|
}
|
||||||
// Append partial block
|
// Append partial block
|
||||||
|
@ -22,6 +22,10 @@ func (f *file) createsHole(plainSize uint64, off int64) bool {
|
|||||||
func (f *file) zeroPad(plainSize uint64) fuse.Status {
|
func (f *file) zeroPad(plainSize uint64) fuse.Status {
|
||||||
lastBlockLen := plainSize % f.contentEnc.PlainBS()
|
lastBlockLen := plainSize % f.contentEnc.PlainBS()
|
||||||
missing := f.contentEnc.PlainBS() - lastBlockLen
|
missing := f.contentEnc.PlainBS() - lastBlockLen
|
||||||
|
if missing == 0 {
|
||||||
|
// Already block-aligned
|
||||||
|
return fuse.OK
|
||||||
|
}
|
||||||
pad := make([]byte, missing)
|
pad := make([]byte, missing)
|
||||||
tlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing)
|
tlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing)
|
||||||
_, status := f.doWrite(pad, int64(plainSize))
|
_, status := f.doWrite(pad, int64(plainSize))
|
||||||
|
@ -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) {
|
func TestTruncate(t *testing.T) {
|
||||||
fn := test_helpers.DefaultPlainDir + "/truncate"
|
fn := test_helpers.DefaultPlainDir + "/truncate"
|
||||||
file, err := os.Create(fn)
|
file, err := os.Create(fn)
|
||||||
@ -143,6 +145,31 @@ func TestTruncate(t *testing.T) {
|
|||||||
if test_helpers.Md5fn(fn) != "620f0b67a91f7f74151bc5be745b7110" {
|
if test_helpers.Md5fn(fn) != "620f0b67a91f7f74151bc5be745b7110" {
|
||||||
t.Errorf("wrong content")
|
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) {
|
func TestAppend(t *testing.T) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user