Skip to content

Commit

Permalink
fusefronted: optimize NFS streaming writes by saving one Stat()
Browse files Browse the repository at this point in the history
Stat() calls are expensive on NFS as they need a full network
round-trip. We detect when a write immediately follows the
last one and skip the Stat in this case because the write
cannot create a file hole.

On my (slow) NAS, this takes the write speed from 24MB/s to
41MB/s.
  • Loading branch information
rfjakob committed Oct 28, 2016
1 parent 9b71352 commit a08d55f
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 4 deletions.
33 changes: 29 additions & 4 deletions internal/fusefrontend/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log"
"os"
"sync"
"sync/atomic"
"syscall"
"time"

Expand Down Expand Up @@ -43,6 +44,11 @@ type file struct {
header *contentenc.FileHeader
// go-fuse nodefs.loopbackFile
loopbackFile nodefs.File
// Store what the last byte was written
lastWrittenOffset int64
// The opCount is used to judge whether "lastWrittenOffset" is still
// guaranteed to be correct.
lastOpCount uint64
}

// NewFile returns a new go-fuse File instance.
Expand Down Expand Up @@ -282,6 +288,16 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) {
return written, status
}

// isConsecutiveWrite returns true if the current write
// directly (in time and space) follows the last write.
// This is an optimisation for streaming writes on NFS where a
// Stat() call is very expensive.
// The caller must "wlock.lock(f.ino)" otherwise this check would be racy.
func (f *file) isConsecutiveWrite(off int64) bool {
opCount := atomic.LoadUint64(&wlock.opCount)
return opCount == f.lastOpCount+1 && off == f.lastWrittenOffset+1
}

// Write - FUSE call
//
// If the write creates a hole, pads the file to the next block boundary.
Expand All @@ -299,11 +315,20 @@ func (f *file) Write(data []byte, off int64) (uint32, fuse.Status) {
defer wlock.unlock(f.ino)
tlog.Debug.Printf("ino%d: FUSE Write: offset=%d length=%d", f.ino, off, len(data))
// If the write creates a file hole, we have to zero-pad the last block.
status := f.writePadHole(off)
if !status.Ok() {
return 0, status
// But if the write directly follows an earlier write, it cannot create a
// hole, and we can save one Stat() call.
if !f.isConsecutiveWrite(off) {
status := f.writePadHole(off)
if !status.Ok() {
return 0, status
}
}
n, status := f.doWrite(data, off)
if status.Ok() {
f.lastOpCount = atomic.LoadUint64(&wlock.opCount)
f.lastWrittenOffset = off + int64(len(data)) - 1
}
return f.doWrite(data, off)
return n, status
}

// Release - FUSE call, close file
Expand Down
8 changes: 8 additions & 0 deletions internal/fusefrontend/write_lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fusefrontend

import (
"sync"
"sync/atomic"
)

func init() {
Expand All @@ -20,6 +21,12 @@ var wlock wlockMap
// 2) lock ... unlock ...
// 3) unregister
type wlockMap struct {
// Counts lock() calls. As every operation that modifies a file should
// call it, this effectively serves as a write-operation counter.
// The variable is accessed without holding any locks so atomic operations
// must be used. It must be the first element of the struct to guarantee
// 64-bit alignment.
opCount uint64
// Protects map access
sync.Mutex
inodeLocks map[uint64]*refCntMutex
Expand Down Expand Up @@ -62,6 +69,7 @@ func (w *wlockMap) unregister(ino uint64) {

// lock retrieves the entry for "ino" and locks it.
func (w *wlockMap) lock(ino uint64) {
atomic.AddUint64(&w.opCount, 1)
w.Lock()
r := w.inodeLocks[ino]
w.Unlock()
Expand Down

0 comments on commit a08d55f

Please sign in to comment.