Skip to content

Commit 911e8cc

Browse files
committed
perf(local): eliminate per-entry allocations in stat scheduler worker path
Reduce heap allocation churn in the hot stat-worker path by introducing per-worker reusable state and removing unnecessary os.File wrappers. Per-worker scratch buffers: - Result slice: workers reuse a goroutine-local []statFileInfo scratch instead of allocating per-microbatch. Publish still copies under mutex with lease validation, preserving stale-completion safety. - Name buffer: custom fstatat using unix.Syscall6(SYS_NEWFSTATAT) with a per-worker [256]byte buffer replaces unix.Fstatat's per-call ByteSliceFromString allocation. Uses Syscall6 (not RawSyscall6) for CephFS scheduler awareness. - ownedEntries: scratch-recycled on listController following the existing resultsScratch/partsScratch pattern. Raw fd lifecycle: - openDirAtReadFD returns raw int fd instead of *os.File, eliminating per-batch File struct, finalizer registration, and poll init overhead. - closeStatDirFD uses syscall.Close for cross-platform compatibility. - All close/drain/error paths updated with idempotent guard. Scratch capture timing: - Moved resultsScratch/partsScratch captures to after Wait() returns, matching ownedEntriesScratch and eliminating a latent aliasing window during the Schedule-to-Wait period. Estimated savings: ~500-1000 MB cumulative allocation reduction and ~3-8M fewer heap objects per listing run on CephFS workloads. Signed-off-by: Anagh Kumar Baranwal <6824881+darthShadow@users.noreply.github.com>
1 parent db185b1 commit 911e8cc

12 files changed

+307
-166
lines changed

backend/local/list.go

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"runtime"
1010
"slices"
11+
"syscall"
1112
"time"
1213

1314
"github.com/rclone/rclone/fs"
@@ -31,19 +32,30 @@ type cachedDirEntry struct {
3132
useStatFD bool
3233
}
3334

35+
const invalidStatDirFD = -1
36+
3437
type readResult struct {
3538
entries []os.DirEntry
3639
err error
37-
statDir *os.File
40+
statDir int
41+
}
42+
43+
func newReadResult() readResult {
44+
return readResult{statDir: invalidStatDirFD}
45+
}
46+
47+
func closeStatDirFD(fd *int) {
48+
if fd == nil || *fd < 0 {
49+
return
50+
}
51+
_ = syscall.Close(*fd)
52+
*fd = invalidStatDirFD
3853
}
3954

4055
// closeReadResultStatDir releases the batch-scoped stat fd when a ReadDir
4156
// batch exits before handing ownership to a batchController.
4257
func closeReadResultStatDir(batch *readResult) {
43-
if batch.statDir != nil {
44-
_ = batch.statDir.Close()
45-
batch.statDir = nil
46-
}
58+
closeStatDirFD(&batch.statDir)
4759
}
4860

4961
func stopTimer(timer *time.Timer) {
@@ -186,8 +198,8 @@ func (f *Fs) listFileInfos(ctx context.Context, fd *os.File, openDir func() (*os
186198
}
187199
return cachedDirEntry{DirEntry: entry}, true
188200
}
189-
cachedStatFunc := func(entry *cachedDirEntry) (os.FileInfo, error) {
190-
return statFunc(entry.DirEntry), nil
201+
cachedStatFunc := func(entry *cachedDirEntry, nameBuf []byte) (os.FileInfo, []byte, error) {
202+
return statFunc(entry.DirEntry), nameBuf, nil
191203
}
192204
fis, err := f.listCachedFileInfos(ctx, fd, openDir, cachedPreFilter, cachedStatFunc)
193205
if err != nil {
@@ -207,7 +219,7 @@ func (f *Fs) listFileInfos(ctx context.Context, fd *os.File, openDir func() (*os
207219
// returns exactly readDirBatchSize entries (indicating more may follow), later
208220
// batches are read through prefetchReader so ReadDir for batch N+1 overlaps
209221
// stat work for batch N.
210-
func (f *Fs) listCachedFileInfos(ctx context.Context, fd *os.File, openDir func() (*os.File, error), preFilter func(os.DirEntry) (cachedDirEntry, bool), statFunc func(entry *cachedDirEntry) (os.FileInfo, error)) (allFis []statFileInfo, err error) {
222+
func (f *Fs) listCachedFileInfos(ctx context.Context, fd *os.File, openDir func() (*os.File, error), preFilter func(os.DirEntry) (cachedDirEntry, bool), statFunc func(entry *cachedDirEntry, nameBuf []byte) (os.FileInfo, []byte, error)) (allFis []statFileInfo, err error) {
211223
const readDirBatchSize = 1024
212224

213225
defer func() {
@@ -301,7 +313,7 @@ func (f *Fs) listCachedFileInfos(ctx context.Context, fd *os.File, openDir func(
301313
}
302314

303315
var entries []cachedDirEntry
304-
batch := readResult{}
316+
batch := newReadResult()
305317
batch.entries, batch.err = fd.ReadDir(readDirBatchSize)
306318
if len(batch.entries) > 0 {
307319
// Give this ReadDir batch its own stat handle tied to the same opened
@@ -429,30 +441,30 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
429441
return cachedEntry, slices.Contains(filter.Opt.ExcludeFile, entry.Name())
430442
}
431443

432-
statFunc := func(entry *cachedDirEntry) (os.FileInfo, error) {
444+
statFunc := func(entry *cachedDirEntry, nameBuf []byte) (os.FileInfo, []byte, error) {
433445
entryType := entry.Type()
434446
if includeDirFn != nil && entryType.IsDir() {
435447
include, inclErr := includeDirFn(entry.newRemote)
436448
if inclErr != nil {
437449
fs.Infof(entry.newRemote, "directory exclusion check failed: %v", inclErr)
438450
}
439451
if !include {
440-
return nil, nil
452+
return nil, nameBuf, nil
441453
}
442454
}
443-
fi, fierr := statDirEntry(entry)
455+
fi, nextNameBuf, fierr := statDirEntry(entry, nameBuf)
444456
if os.IsNotExist(fierr) {
445-
return nil, nil
457+
return nil, nextNameBuf, nil
446458
}
447459
if fierr != nil {
448460
if useFilter && !filter.IncludeRemote(entry.newRemote) {
449-
return nil, nil
461+
return nil, nextNameBuf, nil
450462
}
451463
namepath := join.FilePathJoin(fsDirPath, entry.Name())
452464
fierr = fmt.Errorf("failed to get info about directory entry %q: %w", namepath, fierr)
453465
fs.Errorf(dir, "%v", fierr)
454466
_ = accounting.Stats(ctx).Error(fserrors.NoRetryError(fierr))
455-
return nil, nil
467+
return nil, nextNameBuf, nil
456468
}
457469
if entryType == 0 && f.opt.SkipRecent {
458470
if directoryRecentlyChanged {
@@ -461,12 +473,12 @@ func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err e
461473
!fileCTime.After(timeNow.Add(1*time.Hour)) &&
462474
fileCTime.Add(5*time.Minute).After(timeNow)
463475
if fileRecentlyChanged {
464-
return nil, nil
476+
return nil, nextNameBuf, nil
465477
}
466478
}
467479
}
468480

469-
return fi, nil
481+
return fi, nextNameBuf, nil
470482
}
471483
fis, err := f.listCachedFileInfos(ctx, fd, openDir, preFilter, statFunc)
472484
if err != nil {

backend/local/list_fstatat_linux.go

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
// This file implements the linux/amd64 fast-stat path used by the sequential
44
// listing loop. openDirAtReadFD opens "." relative to the listing read fd to
5-
// create a batch-scoped stat handle, listStatDirFD snapshots that handle's raw
6-
// dirfd once per batch, and statDirEntry uses fstatat(dirfd, name,
5+
// create a batch-scoped raw stat dirfd, listStatDirFD validates that raw fd
6+
// once per batch, and statDirEntry uses fstatat(dirfd, name,
77
// AT_SYMLINK_NOFOLLOW) without constructing absolute paths. Other platforms use
88
// list_fstatat_other.go and fall back to entry.Info().
99

1010
package local
1111

1212
import (
1313
"os"
14+
"runtime"
15+
"strings"
1416
"syscall"
1517
"time"
18+
"unsafe"
1619

1720
"golang.org/x/sys/unix"
1821
)
@@ -89,15 +92,38 @@ func syscallStatFromUnix(st unix.Stat_t) syscall.Stat_t {
8992
}
9093
}
9194

92-
func fstatatNoFollow(dirfd int, name string) (os.FileInfo, error) {
95+
func fstatatNoFollow(dirfd int, name string, nameBuf []byte) (os.FileInfo, []byte, error) {
96+
if strings.IndexByte(name, 0) >= 0 {
97+
return nil, nameBuf, syscall.EINVAL
98+
}
99+
100+
need := len(name) + 1
101+
if cap(nameBuf) < need {
102+
nameBuf = make([]byte, need)
103+
} else {
104+
nameBuf = nameBuf[:need]
105+
}
106+
copy(nameBuf, name)
107+
nameBuf[len(name)] = 0
108+
93109
var st unix.Stat_t
94110
for {
95-
err := unix.Fstatat(dirfd, name, &st, unix.AT_SYMLINK_NOFOLLOW)
96-
if err == syscall.EINTR {
111+
_, _, errno := unix.Syscall6(
112+
unix.SYS_NEWFSTATAT,
113+
uintptr(dirfd),
114+
uintptr(unsafe.Pointer(&nameBuf[0])),
115+
uintptr(unsafe.Pointer(&st)),
116+
uintptr(unix.AT_SYMLINK_NOFOLLOW),
117+
0,
118+
0,
119+
)
120+
runtime.KeepAlive(nameBuf)
121+
runtime.KeepAlive(&st)
122+
if errno == syscall.EINTR {
97123
continue
98124
}
99-
if err != nil {
100-
return nil, err
125+
if errno != 0 {
126+
return nil, nameBuf[:0], errno
101127
}
102128
break
103129
}
@@ -110,19 +136,19 @@ func fstatatNoFollow(dirfd int, name string) (os.FileInfo, error) {
110136
size: st.Size,
111137
modTime: modTime,
112138
stat: syscallStatFromUnix(st),
113-
}, nil
139+
}, nameBuf[:0], nil
114140
}
115141

116142
// openDirAtReadFD opens "." relative to the active listing fd so one ReadDir
117143
// batch gets its own directory handle for fstatat work.
118-
func openDirAtReadFD(fd *os.File) (*os.File, error) {
144+
func openDirAtReadFD(fd *os.File) (int, error) {
119145
if fd == nil {
120-
return nil, os.ErrClosed
146+
return invalidStatDirFD, os.ErrClosed
121147
}
122148

123149
rawfd := fd.Fd()
124150
if rawfd == ^uintptr(0) {
125-
return nil, os.ErrClosed
151+
return invalidStatDirFD, os.ErrClosed
126152
}
127153

128154
for {
@@ -131,30 +157,27 @@ func openDirAtReadFD(fd *os.File) (*os.File, error) {
131157
continue
132158
}
133159
if err != nil {
134-
return nil, err
160+
return invalidStatDirFD, err
135161
}
136-
return os.NewFile(uintptr(statFD), fd.Name()), nil
162+
return statFD, nil
137163
}
138164
}
139165

140-
// listStatDirFD snapshots the raw dirfd from the batch-owned stat handle so the
141-
// batch can reuse it for every entry without reacquiring it per stat call.
142-
func listStatDirFD(fd *os.File) (int, bool) {
143-
if fd == nil {
144-
return 0, false
145-
}
146-
rawfd := fd.Fd()
147-
if rawfd == ^uintptr(0) {
148-
return 0, false
166+
// listStatDirFD validates the batch-owned raw dirfd before the batch snapshots
167+
// it into each cached entry.
168+
func listStatDirFD(fd int) (int, bool) {
169+
if fd < 0 {
170+
return invalidStatDirFD, false
149171
}
150-
return int(rawfd), true
172+
return fd, true
151173
}
152174

153175
// statDirEntry stats one cached entry through the batch-scoped dirfd when
154176
// available, otherwise it falls back to the DirEntry.Info path.
155-
func statDirEntry(entry *cachedDirEntry) (os.FileInfo, error) {
177+
func statDirEntry(entry *cachedDirEntry, nameBuf []byte) (os.FileInfo, []byte, error) {
156178
if entry.useStatFD {
157-
return fstatatNoFollow(entry.statDirFD, entry.Name())
179+
return fstatatNoFollow(entry.statDirFD, entry.Name(), nameBuf)
158180
}
159-
return entry.Info()
181+
fi, err := entry.Info()
182+
return fi, nameBuf, err
160183
}

0 commit comments

Comments
 (0)