diff options
| author | Marios Titas | 2026-08-15 23:03:54 +0300 |
|---|---|---|
| committer | Jakob Unterwurzacher | 2026-09-01 20:51:36 +0200 |
| commit | 5b2881e7cbc347eb379d35c3ae1f491bef8f227f (patch) | |
| tree | 7ba317b34cf943c2c9181597668de8a30a0e039c | |
| parent | 9c2130d6c2e54c45c6787fbc49a896a8544a5e47 (diff) | |
cli: use READDIR by default
Avoid eager per-entry metadata work for names-only listings, and
add -readdirplus for workloads that benefit from eager attributes.
Co-authored-by: Cursor <cursoragent@cursor.com>
| -rw-r--r-- | Documentation/MANPAGE.md | 26 | ||||
| -rw-r--r-- | cli_args.go | 3 | ||||
| -rw-r--r-- | cli_args_test.go | 12 | ||||
| -rw-r--r-- | mount.go | 1 | ||||
| -rw-r--r-- | tests/cli/cli_test.go | 19 | ||||
| -rw-r--r-- | tests/matrix/main_test.go | 1 |
6 files changed, 61 insertions, 1 deletions
diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 3ef6500..e306cf3 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -386,6 +386,32 @@ Only applicable to reverse mode. Limitation: Mounted single files (yes this is possible) are NOT hidden. +#### -readdirplus +Enable FUSE `READDIRPLUS` on Linux. It is disabled by default, so +attributes are fetched only when an application requests them. + +`READDIRPLUS` requests attributes for every entry during a directory +listing. This can improve metadata-heavy workloads such as `ls -l`, +but adds unnecessary work to names-only listings. With the default +cache timeouts (without `-sharedstorage`), enabling `READDIRPLUS` made +metadata-heavy listings 1.36x faster in a local 100,000-file directory +and 1.82x faster in a single run over a 1,000,000-file NFS directory. +It made names-only listings 6.5x and 18.2x slower respectively. + +The default also applies to reverse mode. Backup and synchronization +tools that inspect metadata for most entries may benefit from +`-readdirplus`. + +This option can also be combined with `-sharedstorage`. Because that +mode disables kernel attribute caching, whether `READDIRPLUS` helps +depends on the workload and backing storage. + +On platforms other than Linux, this option is accepted but has no +effect. + +For benchmarks and more details, see +https://github.com/rfjakob/gocryptfs/issues/1026 . + #### -rw, -ro Mount the filesystem read-write (`-rw`, default) or read-only (`-ro`). If both are specified, `-ro` takes precedence. diff --git a/cli_args.go b/cli_args.go index 707b453..52061e3 100644 --- a/cli_args.go +++ b/cli_args.go @@ -29,7 +29,7 @@ type argContainer struct { debug, init, zerokey, fusedebug, openssl, passwd, fg, version, plaintextnames, quiet, nosyslog, wpanic, longnames, allow_other, reverse, aessiv, nonempty, raw64, - noprealloc, speed, hkdf, serialize_reads, hh, info, + noprealloc, readdirplus, speed, hkdf, serialize_reads, hh, info, sharedstorage, fsck, one_file_system, deterministic_names, xchacha, noxattr bool // Mount options with opposites @@ -178,6 +178,7 @@ func parseCliOpts(osArgs []string) (args argContainer) { flagSet.BoolVar(&args.nonempty, "nonempty", false, "Allow mounting over non-empty directories") flagSet.BoolVar(&args.raw64, "raw64", true, "Use unpadded base64 for file names") flagSet.BoolVar(&args.noprealloc, "noprealloc", false, "Disable preallocation before writing") + flagSet.BoolVar(&args.readdirplus, "readdirplus", false, "Enable FUSE READDIRPLUS (Linux only)") flagSet.BoolVar(&args.speed, "speed", false, "Run crypto speed test") flagSet.BoolVar(&args.hkdf, "hkdf", true, "Use HKDF as an additional key derivation step") flagSet.BoolVar(&args.serialize_reads, "serialize_reads", false, "Try to serialize read operations") diff --git a/cli_args_test.go b/cli_args_test.go index 4fb01ac..d7e28ab 100644 --- a/cli_args_test.go +++ b/cli_args_test.go @@ -157,6 +157,18 @@ func TestParseCliOpts(t *testing.T) { }...) o = defaultArgs + o.readdirplus = true + testcases = append(testcases, []testcaseContainer{ + { + i: []string{"gocryptfs", "-readdirplus"}, + o: o, + }, { + i: []string{"gocryptfs", "-o", "readdirplus"}, + o: o, + }, + }...) + + o = defaultArgs o.exclude = []string{"foo", "bar", "baz,boe"} testcases = append(testcases, []testcaseContainer{ { @@ -390,6 +390,7 @@ func initGoFuse(rootNode fs.InodeEmbedder, args *argContainer) *fuse.Server { // Enable go-fuse warnings fuseOpts.Logger = log.New(os.Stderr, "go-fuse: ", log.Lmicroseconds) fuseOpts.MountOptions = fuse.MountOptions{ + DisableReadDirPlus: !args.readdirplus, // Writes and reads are usually capped at 128kiB on Linux through // the FUSE_MAX_PAGES_PER_REQ kernel constant in fuse_i.h. Our // sync.Pool buffer pools are sized acc. to the default. Users may set diff --git a/tests/cli/cli_test.go b/tests/cli/cli_test.go index 01cc3b7..472941d 100644 --- a/tests/cli/cli_test.go +++ b/tests/cli/cli_test.go @@ -991,6 +991,25 @@ func TestInitNotEmpty(t *testing.T) { } } +// TestReaddirplus checks that mounting and listing work with -readdirplus. +func TestReaddirplus(t *testing.T) { + dir := test_helpers.InitFS(t) + mnt := dir + ".mnt" + test_helpers.MountOrFatal(t, dir, mnt, "-extpass=echo test", "-readdirplus") + defer test_helpers.UnmountPanic(mnt) + + if err := os.WriteFile(mnt+"/file", nil, 0600); err != nil { + t.Fatal(err) + } + entries, err := os.ReadDir(mnt) + if err != nil { + t.Fatal(err) + } + if len(entries) != 1 || entries[0].Name() != "file" { + t.Fatalf("unexpected directory entries: %v", entries) + } +} + // TestSharedstorage checks that `-sharedstorage` shows stable inode numbers to // userspace despite having hard link tracking disabled func TestSharedstorage(t *testing.T) { diff --git a/tests/matrix/main_test.go b/tests/matrix/main_test.go index cf0b6c4..126adaf 100644 --- a/tests/matrix/main_test.go +++ b/tests/matrix/main_test.go @@ -59,6 +59,7 @@ func TestMain(m *testing.M) { {false, "auto", false, true, nil}, // -serialize_reads {false, "auto", false, false, []string{"-serialize_reads"}}, + {false, "auto", false, false, []string{"-readdirplus"}}, {false, "auto", false, false, []string{"-sharedstorage"}}, {false, "auto", false, false, []string{"-deterministic-names"}}, // Test xchacha with and without openssl |
