diff --git a/mount.test b/mount.test new file mode 100755 index 000000000..1aa1d3967 Binary files /dev/null and b/mount.test differ diff --git a/weed/mount/dir_sink.go b/weed/mount/dir_sink.go index 81cb6cd3a..1299e4f35 100644 --- a/weed/mount/dir_sink.go +++ b/weed/mount/dir_sink.go @@ -15,6 +15,10 @@ type DirEntrySink interface { // AddEntryPlus is AddEntry for readdirplus, returning the attribute block // to fill in, or nil once the sink is full. AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut + + // TakesLookupRef reports whether AddEntryPlus hands the sink a reference the + // mount must hold until a FORGET returns it. + TakesLookupRef() bool } // fuseDirEntryList adapts the kernel reply buffer to DirEntrySink. @@ -30,6 +34,8 @@ func (l fuseDirEntryList) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut { return l.AddDirLookupEntry(entry) } +func (l fuseDirEntryList) TakesLookupRef() bool { return true } + // ReadDirectoryInto runs a readdir against sink. ReadDir and ReadDirPlus are // this with the kernel reply buffer as the sink. func (wfs *WFS) ReadDirectoryInto(input *fuse.ReadIn, sink DirEntrySink, isPlusMode bool) fuse.Status { diff --git a/weed/mount/inode_to_path.go b/weed/mount/inode_to_path.go index e0129db1a..295b1a804 100644 --- a/weed/mount/inode_to_path.go +++ b/weed/mount/inode_to_path.go @@ -137,6 +137,35 @@ func (i *InodeToPath) Lookup(path util.FullPath, unixTime int64, isDirectory boo return inode } +// IncrementNlookup takes one more reference on an inode already in the table, +// reporting false if it is not there. +func (i *InodeToPath) IncrementNlookup(inode uint64) bool { + i.Lock() + defer i.Unlock() + entry, found := i.inode2path[inode] + if !found { + return false + } + entry.nlookup++ + return true +} + +// InodeForListing returns the inode number a readdir should report for path +// without entering it in the table. Nothing is reserved, so the collision probe +// Lookup does is skipped: the worst case is a repeated st_ino in one listing. +func (i *InodeToPath) InodeForListing(path util.FullPath, unixTime int64, possibleInode uint64) uint64 { + i.RLock() + inode, found := i.path2inode[path] + i.RUnlock() + if found { + return inode + } + if possibleInode != 0 { + return possibleInode + } + return path.AsInode(unixTime) +} + func (i *InodeToPath) AllocateInode(path util.FullPath, unixTime int64) uint64 { if path == "/" { return 1 diff --git a/weed/mount/weedfs_dir_read.go b/weed/mount/weedfs_dir_read.go index dbdba0e97..0949a2e0f 100644 --- a/weed/mount/weedfs_dir_read.go +++ b/weed/mount/weedfs_dir_read.go @@ -170,12 +170,21 @@ func (wfs *WFS) doReadDirectory(input *fuse.ReadIn, out DirEntrySink, isPlusMode wfs.inodeToPath.TouchDirectory(dirPath) var dirEntry fuse.DirEntry + // Only a reference makes a child worth entering in the inode table: without + // one nothing ever arrives to take the entry back out again. + takesLookupRef := isPlusMode && out.TakesLookupRef() // index is the position in entryStream, used to calculate the offset for next readdir processEachEntryFn := func(entry *filer.Entry, index int64) bool { dirEntry.Name = entry.Name() dirEntry.Mode = toSyscallMode(entry.Mode) - inode := wfs.inodeToPath.Lookup(dirPath.Child(dirEntry.Name), entry.Crtime.Unix(), entry.IsDirectory(), len(entry.HardLinkId) > 0, entry.Inode, false) + childPath := dirPath.Child(dirEntry.Name) + var inode uint64 + if takesLookupRef { + inode = wfs.inodeToPath.Lookup(childPath, entry.Crtime.Unix(), entry.IsDirectory(), len(entry.HardLinkId) > 0, entry.Inode, false) + } else { + inode = wfs.inodeToPath.InodeForListing(childPath, entry.Crtime.Unix(), entry.Inode) + } dirEntry.Ino = inode // Set Off to the next offset so client can resume from correct position @@ -191,11 +200,15 @@ func (wfs *WFS) doReadDirectory(input *fuse.ReadIn, out DirEntrySink, isPlusMode return false } if fh, found := wfs.fhMap.FindFileHandle(inode); found { - glog.V(4).Infof("readdir opened file %s", dirPath.Child(dirEntry.Name)) + glog.V(4).Infof("readdir opened file %s", childPath) entry = filer.FromPbEntry(string(dirPath), fh.GetEntry().GetEntry()) } wfs.outputFilerEntry(entryOut, inode, entry) - wfs.inodeToPath.Lookup(dirPath.Child(dirEntry.Name), entry.Crtime.Unix(), entry.IsDirectory(), len(entry.HardLinkId) > 0, entry.Inode, true) + // Taken only once the entry is really in the sink, so one that did not + // fit leaves no reference behind. The fallback covers a racing Forget. + if takesLookupRef && !wfs.inodeToPath.IncrementNlookup(inode) { + wfs.inodeToPath.Lookup(childPath, entry.Crtime.Unix(), entry.IsDirectory(), len(entry.HardLinkId) > 0, entry.Inode, true) + } } return true } diff --git a/weed/mount/weedfs_dir_read_bench_test.go b/weed/mount/weedfs_dir_read_bench_test.go new file mode 100644 index 000000000..0681f72d1 --- /dev/null +++ b/weed/mount/weedfs_dir_read_bench_test.go @@ -0,0 +1,222 @@ +package mount + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/seaweedfs/go-fuse/v2/fuse" + "github.com/seaweedfs/seaweedfs/weed/filer" + "github.com/seaweedfs/seaweedfs/weed/mount/meta_cache" + "github.com/seaweedfs/seaweedfs/weed/pb" + "github.com/seaweedfs/seaweedfs/weed/util" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +const benchDirEntryCount = 200000 + +// benchSink drains a readdir the way a front end does. sinkLimit is how many +// entries one round accepts, standing in for the kernel reply buffer or the +// WinFsp adapter's batch. +type benchSink struct { + plus bool + takesRef bool + sinkLimit int + count int + inodes []uint64 + lastOff uint64 + attrs []fuse.EntryOut +} + +func (s *benchSink) reset() { + s.count = 0 + s.inodes = s.inodes[:0] + s.attrs = s.attrs[:0] +} + +func (s *benchSink) AddEntry(entry fuse.DirEntry) bool { + if s.count >= s.sinkLimit { + return false + } + s.count++ + s.lastOff = entry.Off + s.inodes = append(s.inodes, entry.Ino) + return true +} + +func (s *benchSink) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut { + if s.count >= s.sinkLimit { + return nil + } + s.count++ + s.lastOff = entry.Off + s.inodes = append(s.inodes, entry.Ino) + s.attrs = append(s.attrs, fuse.EntryOut{}) + return &s.attrs[len(s.attrs)-1] +} + +func (s *benchSink) TakesLookupRef() bool { return s.takesRef } + +func inodeTableSize(i *InodeToPath) int { + i.RLock() + defer i.RUnlock() + return len(i.inode2path) +} + +func newBenchWFS(tb testing.TB, dir util.FullPath, n int) *WFS { + tb.Helper() + + uidGidMapper, err := meta_cache.NewUidGidMapper("", "") + if err != nil { + tb.Fatalf("uid/gid mapper: %v", err) + } + + root := util.FullPath("/") + option := &Option{ + ChunkSizeLimit: 1024, + ConcurrentReaders: 1, + VolumeServerAccess: "filerProxy", + FilerAddresses: []pb.ServerAddress{pb.NewServerAddressWithGrpcPort("127.0.0.1:1", 1)}, + GrpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()), + FilerMountRootPath: "/", + MountUid: 99, + MountGid: 100, + MountMode: 0o777, + MountMtime: time.Now(), + MountCtime: time.Now(), + UidGidMapper: uidGidMapper, + } + + wfs := &WFS{ + option: option, + signature: 1, + inodeToPath: NewInodeToPath(root, 0), + fhMap: NewFileHandleToInode(), + dhMap: NewDirectoryHandleToInode(), + fhLockTable: util.NewLockTable[FileHandleId](), + hardLinkLockTable: util.NewLockTable[string](), + } + wfs.metaCache = meta_cache.NewMetaCache( + filepath.Join(tb.TempDir(), "meta"), + uidGidMapper, + root, + false, + func(path util.FullPath) { wfs.inodeToPath.MarkChildrenCached(path) }, + func(path util.FullPath) bool { return wfs.inodeToPath.IsChildrenCached(path) }, + func(meta_cache.EntryInvalidation) {}, + nil, + ) + tb.Cleanup(wfs.metaCache.Shutdown) + + now := time.Now() + ctx := context.Background() + if err := wfs.metaCache.InsertEntry(ctx, &filer.Entry{ + FullPath: dir, + Attr: filer.Attr{Mode: os.ModeDir | 0o755, Mtime: now, Crtime: now, Uid: 99, Gid: 100}, + }, 0); err != nil { + tb.Fatalf("insert dir: %v", err) + } + for i := 0; i < n; i++ { + child := dir.Child(fmt.Sprintf("image-%08d.jpg", i)) + if err := wfs.metaCache.InsertEntry(ctx, &filer.Entry{ + FullPath: child, + // The filer stamps an inode on every entry it stores, so a listing + // arrives with one and never has to derive its own. + Attr: filer.Attr{Mode: 0o644, Mtime: now, Crtime: now, Uid: 99, Gid: 100, FileSize: 4096, Inode: child.AsInode(now.Unix())}, + }, 0); err != nil { + tb.Fatalf("insert entry %d: %v", i, err) + } + } + + // Mark the tree cached so the listing is served from the meta cache and no + // filer client is dialled. + wfs.inodeToPath.MarkChildrenCached(root) + wfs.inodeToPath.Lookup(dir, now.Unix(), true, false, 0, true) + wfs.inodeToPath.MarkChildrenCached(dir) + + return wfs +} + +// walkOnce enumerates the whole directory the way a front end does: repeated +// rounds against one handle until the listing runs dry, returning whatever +// references the round took. +func walkOnce(tb testing.TB, wfs *WFS, dirInode uint64, sink *benchSink, forgets bool) int { + dhid, _ := wfs.AcquireDirectoryHandle() + defer wfs.ReleaseDirectoryHandle(dhid) + + total := 0 + offset := uint64(0) + for { + sink.reset() + status := wfs.doReadDirectory(&fuse.ReadIn{ + InHeader: fuse.InHeader{NodeId: dirInode}, + Fh: uint64(dhid), + Offset: offset, + Size: 1 << 20, + }, sink, sink.plus) + if status != fuse.OK { + tb.Fatalf("readdir: %v", status) + } + if sink.count == 0 { + return total + } + total += sink.count + if forgets { + // HasInode keeps this to the entries that really hold a reference, + // so a listing that took none is not charged for a bogus Forget. + for _, ino := range sink.inodes { + if wfs.inodeToPath.HasInode(ino) { + wfs.Forget(ino, 1) + } + } + } + if sink.lastOff <= offset { + return total + } + offset = sink.lastOff + } +} + +var benchCases = []struct { + name string + plus bool + // ref mirrors the sink's TakesLookupRef. The kernel's reports true whatever + // the mode, exactly as fuseDirEntryList does; it is the mode that decides + // whether a reference is actually granted. + ref bool + // forgets is what the front end does after a round: the kernel returns one + // FORGET per readdirplus entry and none for a plain readdir, and the WinFsp + // adapter hands back everything a round gave it. + forgets bool +}{ + {"kernel_readdir", false, true, false}, + {"kernel_readdirplus", true, true, true}, + {"winfsp_readdirplus", true, false, true}, +} + +func BenchmarkReadDirectory(b *testing.B) { + dir := util.FullPath("/images") + for _, tc := range benchCases { + b.Run(tc.name, func(b *testing.B) { + wfs := newBenchWFS(b, dir, benchDirEntryCount) + dirInode, _ := wfs.inodeToPath.GetInode(dir) + sink := &benchSink{plus: tc.plus, takesRef: tc.ref, sinkLimit: 4096} + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if got := walkOnce(b, wfs, dirInode, sink, tc.forgets); got != benchDirEntryCount+2 { + b.Fatalf("listed %d entries, want %d", got, benchDirEntryCount+2) + } + } + b.StopTimer() + // What the listing left in the inode table, over the root and the + // directory itself. + b.ReportMetric(float64(inodeTableSize(wfs.inodeToPath)), "inodes_left") + }) + } +} diff --git a/weed/mount/winfsp/fs_windows.go b/weed/mount/winfsp/fs_windows.go index 34a5c8dcf..1c18537ae 100644 --- a/weed/mount/winfsp/fs_windows.go +++ b/weed/mount/winfsp/fs_windows.go @@ -651,6 +651,9 @@ func (s *readdirSink) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut { return out } +// WinFsp has no FORGET, and every EntryOut is converted before the round ends. +func (s *readdirSink) TakesLookupRef() bool { return false } + func (w *WinFS) Readdir(path string, fill func(name string, stat *cgofuse.Stat_t, ofst int64) bool, ofst int64, fh uint64) int { inode := w.inodeForHandle(w.dirInodes, fh) if inode == 0 { @@ -699,12 +702,6 @@ func (w *WinFS) Readdir(path string, fill func(name string, stat *cgofuse.Stat_t filled = false } } - // A readdirplus entry carries a reference of its own. Give every one - // of them back, including any the fill above stopped short of, or a - // single walk of a wide directory strands one reference per child. - for _, child := range sink.inodes { - w.forget(child) - } if !filled { return 0 }