2019-01-02 22:32:21 +01:00
|
|
|
package fusefrontend
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"sync"
|
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
|
|
)
|
|
|
|
|
2019-01-04 21:45:03 +01:00
|
|
|
const (
|
2020-05-17 21:37:36 +02:00
|
|
|
// Number of entries in the dirCache.
|
|
|
|
// 20 entries work well for "git stat" on a small git repo on sshfs.
|
2019-01-05 02:10:03 +01:00
|
|
|
// Keep in sync with test_helpers.maxCacheFds !
|
|
|
|
// TODO: How to share this constant without causing an import cycle?
|
2020-05-17 21:37:36 +02:00
|
|
|
dirCacheSize = 20
|
2019-01-04 21:45:03 +01:00
|
|
|
// Enable Lookup/Store/Clear debug messages
|
|
|
|
enableDebugMessages = false
|
|
|
|
// Enable hit rate statistics printing
|
2019-01-04 22:06:28 +01:00
|
|
|
enableStats = false
|
2019-01-04 21:45:03 +01:00
|
|
|
)
|
|
|
|
|
2021-04-04 13:05:47 +02:00
|
|
|
type dirCacheEntry struct {
|
2021-04-03 13:08:28 +02:00
|
|
|
// pointer to the Node this entry belongs to
|
|
|
|
node *Node
|
2019-01-02 22:32:21 +01:00
|
|
|
// fd to the directory (opened with O_PATH!)
|
|
|
|
fd int
|
|
|
|
// content of gocryptfs.diriv in this directory
|
|
|
|
iv []byte
|
2019-01-04 21:45:03 +01:00
|
|
|
}
|
|
|
|
|
2021-04-04 13:05:47 +02:00
|
|
|
func (e *dirCacheEntry) Clear() {
|
2019-01-04 21:45:03 +01:00
|
|
|
// An earlier clear may have already closed the fd, or the cache
|
|
|
|
// has never been filled (fd is 0 in that case).
|
2019-01-05 15:44:32 +01:00
|
|
|
// Note: package ensurefds012, imported from main, guarantees that dirCache
|
|
|
|
// can never get fds 0,1,2.
|
2019-01-04 21:45:03 +01:00
|
|
|
if e.fd > 0 {
|
|
|
|
err := syscall.Close(e.fd)
|
|
|
|
if err != nil {
|
|
|
|
tlog.Warn.Printf("dirCache.Clear: Close failed: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
e.fd = -1
|
2021-04-03 13:08:28 +02:00
|
|
|
e.node = nil
|
2019-01-04 21:45:03 +01:00
|
|
|
e.iv = nil
|
|
|
|
}
|
|
|
|
|
2021-04-04 13:05:47 +02:00
|
|
|
type dirCache struct {
|
2019-01-04 21:45:03 +01:00
|
|
|
sync.Mutex
|
fusefrontend: implement recursive diriv caching
The new contrib/maxlen.bash showed that we have exponential
runtime with respect to directory depth.
The new recursive diriv caching is a lot smarter as it caches
intermediate lookups. maxlen.bash now completes in a few seconds.
xfstests results same as
https://github.com/rfjakob/fuse-xfstests/blob/2d158e4c82be85c15269af77498e353f928f4fab/screenlog.0 :
Failures: generic/035 generic/062 generic/080 generic/093 generic/099 generic/215 generic/285 generic/319 generic/426 generic/444 generic/467 generic/477 generic/523
Failed 13 of 580 tests
benchmark.bash results are identical:
$ ./benchmark.bash
Testing gocryptfs at /tmp/benchmark.bash.BdQ: gocryptfs v2.0.1-17-g6b09bc0; go-fuse v2.1.1-0.20210611132105-24a1dfe6b4f8; 2021-06-25 go1.16.5 linux/amd64
/tmp/benchmark.bash.BdQ.mnt is a mountpoint
WRITE: 262144000 bytes (262 MB, 250 MiB) copied, 0,4821 s, 544 MB/s
READ: 262144000 bytes (262 MB, 250 MiB) copied, 0,266061 s, 985 MB/s
UNTAR: 8,280
MD5: 4,564
LS: 1,745
RM: 2,244
2021-06-25 11:33:18 +02:00
|
|
|
// Expected length of the stored IVs. Only used for sanity checks.
|
|
|
|
// Usually set to 16, but 0 in plaintextnames mode.
|
|
|
|
ivLen int
|
2019-01-04 21:45:03 +01:00
|
|
|
// Cache entries
|
2021-04-04 13:05:47 +02:00
|
|
|
entries [dirCacheSize]dirCacheEntry
|
2019-01-04 21:45:03 +01:00
|
|
|
// Where to store the next entry (index into entries)
|
|
|
|
nextIndex int
|
2019-01-05 02:10:03 +01:00
|
|
|
// On the first Lookup(), the expire thread is started, and this flag is set
|
2019-01-02 22:32:21 +01:00
|
|
|
// to true.
|
|
|
|
expireThreadRunning bool
|
2019-01-04 21:45:03 +01:00
|
|
|
// Hit rate stats. Evaluated and reset by the expire thread.
|
|
|
|
lookups uint64
|
|
|
|
hits uint64
|
2019-01-02 22:32:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Clear clears the cache contents.
|
2021-04-04 13:05:47 +02:00
|
|
|
func (d *dirCache) Clear() {
|
2020-05-17 21:26:56 +02:00
|
|
|
d.dbg("Clear\n")
|
2019-01-02 22:32:21 +01:00
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
2019-01-04 21:45:03 +01:00
|
|
|
for i := range d.entries {
|
|
|
|
d.entries[i].Clear()
|
2019-01-02 22:32:21 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store the entry in the cache. The passed "fd" will be Dup()ed, and the caller
|
|
|
|
// can close their copy at will.
|
2021-04-04 13:05:47 +02:00
|
|
|
func (d *dirCache) Store(node *Node, fd int, iv []byte) {
|
2019-01-05 15:44:32 +01:00
|
|
|
// Note: package ensurefds012, imported from main, guarantees that dirCache
|
|
|
|
// can never get fds 0,1,2.
|
fusefrontend: implement recursive diriv caching
The new contrib/maxlen.bash showed that we have exponential
runtime with respect to directory depth.
The new recursive diriv caching is a lot smarter as it caches
intermediate lookups. maxlen.bash now completes in a few seconds.
xfstests results same as
https://github.com/rfjakob/fuse-xfstests/blob/2d158e4c82be85c15269af77498e353f928f4fab/screenlog.0 :
Failures: generic/035 generic/062 generic/080 generic/093 generic/099 generic/215 generic/285 generic/319 generic/426 generic/444 generic/467 generic/477 generic/523
Failed 13 of 580 tests
benchmark.bash results are identical:
$ ./benchmark.bash
Testing gocryptfs at /tmp/benchmark.bash.BdQ: gocryptfs v2.0.1-17-g6b09bc0; go-fuse v2.1.1-0.20210611132105-24a1dfe6b4f8; 2021-06-25 go1.16.5 linux/amd64
/tmp/benchmark.bash.BdQ.mnt is a mountpoint
WRITE: 262144000 bytes (262 MB, 250 MiB) copied, 0,4821 s, 544 MB/s
READ: 262144000 bytes (262 MB, 250 MiB) copied, 0,266061 s, 985 MB/s
UNTAR: 8,280
MD5: 4,564
LS: 1,745
RM: 2,244
2021-06-25 11:33:18 +02:00
|
|
|
if fd <= 0 || len(iv) != d.ivLen {
|
2019-01-02 22:32:21 +01:00
|
|
|
log.Panicf("Store sanity check failed: fd=%d len=%d", fd, len(iv))
|
|
|
|
}
|
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
2019-01-04 21:45:03 +01:00
|
|
|
e := &d.entries[d.nextIndex]
|
|
|
|
// Round-robin works well enough
|
|
|
|
d.nextIndex = (d.nextIndex + 1) % dirCacheSize
|
2019-01-02 22:32:21 +01:00
|
|
|
// Close the old fd
|
2019-01-04 21:45:03 +01:00
|
|
|
e.Clear()
|
2019-01-02 22:32:21 +01:00
|
|
|
fd2, err := syscall.Dup(fd)
|
|
|
|
if err != nil {
|
|
|
|
tlog.Warn.Printf("dirCache.Store: Dup failed: %v", err)
|
|
|
|
return
|
|
|
|
}
|
2021-04-03 13:08:28 +02:00
|
|
|
d.dbg("dirCache.Store %p fd=%d iv=%x\n", node, fd2, iv)
|
2019-01-04 21:45:03 +01:00
|
|
|
e.fd = fd2
|
2021-04-03 13:08:28 +02:00
|
|
|
e.node = node
|
2019-01-04 21:45:03 +01:00
|
|
|
e.iv = iv
|
2019-01-02 22:32:21 +01:00
|
|
|
// expireThread is started on the first Lookup()
|
|
|
|
if !d.expireThreadRunning {
|
|
|
|
d.expireThreadRunning = true
|
|
|
|
go d.expireThread()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-04 21:45:03 +01:00
|
|
|
// Lookup checks if relPath is in the cache, and returns an (fd, iv) pair.
|
2019-01-02 22:32:21 +01:00
|
|
|
// It returns (-1, nil) if not found. The fd is internally Dup()ed and the
|
|
|
|
// caller must close it when done.
|
2021-04-04 13:05:47 +02:00
|
|
|
func (d *dirCache) Lookup(node *Node) (fd int, iv []byte) {
|
2019-01-02 22:32:21 +01:00
|
|
|
d.Lock()
|
|
|
|
defer d.Unlock()
|
2019-01-04 21:45:03 +01:00
|
|
|
if enableStats {
|
|
|
|
d.lookups++
|
|
|
|
}
|
2021-04-04 13:05:47 +02:00
|
|
|
var e *dirCacheEntry
|
2019-01-04 21:45:03 +01:00
|
|
|
for i := range d.entries {
|
2020-05-17 21:26:56 +02:00
|
|
|
e = &d.entries[i]
|
2019-01-04 21:45:03 +01:00
|
|
|
if e.fd <= 0 {
|
|
|
|
// Cache slot is empty
|
|
|
|
continue
|
|
|
|
}
|
2021-04-03 13:08:28 +02:00
|
|
|
if node != e.node {
|
2019-01-04 21:45:03 +01:00
|
|
|
// Not the right path
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
fd, err = syscall.Dup(e.fd)
|
|
|
|
if err != nil {
|
|
|
|
tlog.Warn.Printf("dirCache.Lookup: Dup failed: %v", err)
|
|
|
|
return -1, nil
|
|
|
|
}
|
|
|
|
iv = e.iv
|
2019-01-04 23:35:48 +01:00
|
|
|
break
|
2019-01-02 22:32:21 +01:00
|
|
|
}
|
2019-01-04 21:45:03 +01:00
|
|
|
if fd == 0 {
|
2021-04-03 13:08:28 +02:00
|
|
|
d.dbg("dirCache.Lookup %p miss\n", node)
|
2019-01-02 22:32:21 +01:00
|
|
|
return -1, nil
|
|
|
|
}
|
2019-01-04 21:45:03 +01:00
|
|
|
if enableStats {
|
|
|
|
d.hits++
|
2019-01-02 22:32:21 +01:00
|
|
|
}
|
fusefrontend: implement recursive diriv caching
The new contrib/maxlen.bash showed that we have exponential
runtime with respect to directory depth.
The new recursive diriv caching is a lot smarter as it caches
intermediate lookups. maxlen.bash now completes in a few seconds.
xfstests results same as
https://github.com/rfjakob/fuse-xfstests/blob/2d158e4c82be85c15269af77498e353f928f4fab/screenlog.0 :
Failures: generic/035 generic/062 generic/080 generic/093 generic/099 generic/215 generic/285 generic/319 generic/426 generic/444 generic/467 generic/477 generic/523
Failed 13 of 580 tests
benchmark.bash results are identical:
$ ./benchmark.bash
Testing gocryptfs at /tmp/benchmark.bash.BdQ: gocryptfs v2.0.1-17-g6b09bc0; go-fuse v2.1.1-0.20210611132105-24a1dfe6b4f8; 2021-06-25 go1.16.5 linux/amd64
/tmp/benchmark.bash.BdQ.mnt is a mountpoint
WRITE: 262144000 bytes (262 MB, 250 MiB) copied, 0,4821 s, 544 MB/s
READ: 262144000 bytes (262 MB, 250 MiB) copied, 0,266061 s, 985 MB/s
UNTAR: 8,280
MD5: 4,564
LS: 1,745
RM: 2,244
2021-06-25 11:33:18 +02:00
|
|
|
if fd <= 0 || len(iv) != d.ivLen {
|
2019-01-04 21:45:03 +01:00
|
|
|
log.Panicf("Lookup sanity check failed: fd=%d len=%d", fd, len(iv))
|
2019-01-02 22:32:21 +01:00
|
|
|
}
|
2021-04-03 13:08:28 +02:00
|
|
|
d.dbg("dirCache.Lookup %p hit fd=%d dup=%d iv=%x\n", node, e.fd, fd, iv)
|
2019-01-04 21:45:03 +01:00
|
|
|
return fd, iv
|
2019-01-02 22:32:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// expireThread is started on the first Lookup()
|
2021-04-04 13:05:47 +02:00
|
|
|
func (d *dirCache) expireThread() {
|
2019-01-02 22:32:21 +01:00
|
|
|
for {
|
2020-05-17 21:37:36 +02:00
|
|
|
time.Sleep(60 * time.Second)
|
2019-01-02 22:32:21 +01:00
|
|
|
d.Clear()
|
2021-04-05 18:20:17 +02:00
|
|
|
d.stats()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// stats prints hit rate statistics and resets the counters. No-op if
|
|
|
|
// enableStats == false.
|
|
|
|
func (d *dirCache) stats() {
|
|
|
|
if !enableStats {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
d.Lock()
|
|
|
|
lookups := d.lookups
|
|
|
|
hits := d.hits
|
|
|
|
d.lookups = 0
|
|
|
|
d.hits = 0
|
|
|
|
d.Unlock()
|
|
|
|
if lookups > 0 {
|
|
|
|
fmt.Printf("dirCache: hits=%3d lookups=%3d, rate=%3d%%\n", hits, lookups, (hits*100)/lookups)
|
2019-01-02 22:32:21 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// dbg prints a debug message. Usually disabled.
|
2021-04-04 13:05:47 +02:00
|
|
|
func (d *dirCache) dbg(format string, a ...interface{}) {
|
2019-01-04 21:45:03 +01:00
|
|
|
if enableDebugMessages {
|
2019-01-02 22:32:21 +01:00
|
|
|
fmt.Printf(format, a...)
|
|
|
|
}
|
|
|
|
}
|