Skip to content

Commit 398254f

Browse files
committed
feat(local): read filesystem I/O block size for optimal operations
Query the filesystem to determine optimal I/O block size rather than using a hardcoded 4096 value. This improves alignment with underlying storage characteristics. Block size detection (Unix): - statfs.Bsize: Filesystem's fundamental block size - stat.Blksize: Preferred I/O block size for the file/directory - Use minimum of both with bounds checking (0 < size <= MaxInt32) - Default to 4096 if detection fails The IOBlockSize is exposed through fs.Usage and propagated to: - FUSE statfs responses (Bsize, Frsize fields) - File attribute block calculations - VFS via GetBlockSizes() returning (dataBlockSize=512, ioBlockSize) Union backend aggregates by taking minimum across all upstreams, ensuring conservative alignment for heterogeneous storage. Windows defaults to 4096 as the statfs equivalent doesn't provide preferred I/O size information. Signed-off-by: Anagh Kumar Baranwal <[email protected]>
1 parent b0f671c commit 398254f

File tree

11 files changed

+185
-50
lines changed

11 files changed

+185
-50
lines changed

backend/local/about_unix.go

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,67 @@ package local
55
import (
66
"context"
77
"fmt"
8+
"math"
89
"os"
9-
"syscall"
1010

1111
"github.com/rclone/rclone/fs"
12+
"golang.org/x/sys/unix"
1213
)
1314

15+
// calculateOptimalBlockSize determines the optimal block size for I/O operations
16+
// by examining both statfs and stat block sizes and applying bounds checking
17+
func calculateOptimalBlockSize(statfsBlockSize, statBlockSize int64) int32 {
18+
const defaultBlockSize = 4096 // reasonable default for modern filesystems
19+
const maxBlockSize = math.MaxInt32 // max int32 value (~2GB)
20+
21+
// Apply bounds checking for statfs block size
22+
if statfsBlockSize <= 0 {
23+
statfsBlockSize = defaultBlockSize
24+
} else if statfsBlockSize > maxBlockSize {
25+
statfsBlockSize = maxBlockSize
26+
}
27+
28+
// Apply bounds checking for stat block size
29+
if statBlockSize <= 0 {
30+
statBlockSize = defaultBlockSize
31+
} else if statBlockSize > maxBlockSize {
32+
statBlockSize = maxBlockSize
33+
}
34+
35+
// Use minimum of both block sizes for optimal IO
36+
return int32(min(statfsBlockSize, statBlockSize))
37+
}
38+
1439
// About gets quota information
1540
func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
16-
var s syscall.Statfs_t
17-
err := syscall.Statfs(f.root, &s)
41+
var unixStatfs unix.Statfs_t
42+
err := unix.Statfs(f.root, &unixStatfs)
1843
if err != nil {
1944
if os.IsNotExist(err) {
2045
return nil, fs.ErrorDirNotFound
2146
}
2247
return nil, fmt.Errorf("failed to read disk usage: %w", err)
2348
}
24-
bs := int64(s.Bsize) // nolint: unconvert
49+
50+
// Get statfs block size (filesystem block size)
51+
statfsBlockSize := int64(unixStatfs.Bsize) // nolint: unconvert
52+
53+
// Get stat block size from a file in the directory (preferred I/O block size)
54+
var statBlockSize int64
55+
var unixStat unix.Stat_t
56+
err = unix.Stat(f.root, &unixStat)
57+
if err == nil {
58+
statBlockSize = int64(unixStat.Blksize) // nolint: unconvert
59+
}
60+
61+
// Calculate optimal block size with bounds checking
62+
bs := calculateOptimalBlockSize(statfsBlockSize, statBlockSize)
63+
2564
usage := &fs.Usage{
26-
Total: fs.NewUsageValue(bs * int64(s.Blocks)), //nolint: unconvert // quota of bytes that can be used
27-
Used: fs.NewUsageValue(bs * int64(s.Blocks-s.Bfree)), //nolint: unconvert // bytes in use
28-
Free: fs.NewUsageValue(bs * int64(s.Bavail)), //nolint: unconvert // bytes which can be uploaded before reaching the quota
65+
Total: fs.NewUsageValue(statfsBlockSize * int64(unixStatfs.Blocks)), //nolint: unconvert // quota of bytes that can be used
66+
Used: fs.NewUsageValue(statfsBlockSize * int64(unixStatfs.Blocks-unixStatfs.Bfree)), //nolint: unconvert // bytes in use
67+
Free: fs.NewUsageValue(statfsBlockSize * int64(unixStatfs.Bavail)), //nolint: unconvert // bytes which can be uploaded before reaching the quota
68+
IOBlockSize: fs.NewUsageValue32(bs), // preferred IO block size for this filesystem
2969
}
3070
return usage, nil
3171
}

backend/local/about_windows.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
3030
return nil, fmt.Errorf("failed to read disk usage: %w", e1)
3131
}
3232
usage := &fs.Usage{
33-
Total: fs.NewUsageValue(total), // quota of bytes that can be used
34-
Used: fs.NewUsageValue(total - free), // bytes in use
35-
Free: fs.NewUsageValue(available), // bytes which can be uploaded before reaching the quota
33+
Total: fs.NewUsageValue(total), // quota of bytes that can be used
34+
Used: fs.NewUsageValue(total - free), // bytes in use
35+
Free: fs.NewUsageValue(available), // bytes which can be uploaded before reaching the quota
36+
IOBlockSize: fs.NewUsageValue32(4096), // use 4KB default for Windows
3637
}
3738
return usage, nil
3839
}

backend/union/union.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -644,12 +644,13 @@ func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, opt
644644
// About gets quota information from the Fs
645645
func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
646646
usage := &fs.Usage{
647-
Total: new(int64),
648-
Used: new(int64),
649-
Trashed: new(int64),
650-
Other: new(int64),
651-
Free: new(int64),
652-
Objects: new(int64),
647+
Total: new(int64),
648+
Used: new(int64),
649+
Trashed: new(int64),
650+
Other: new(int64),
651+
Free: new(int64),
652+
Objects: new(int64),
653+
IOBlockSize: new(int32),
653654
}
654655
for _, u := range f.upstreams {
655656
usg, err := u.About(ctx)
@@ -689,6 +690,15 @@ func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
689690
} else {
690691
usage.Objects = nil
691692
}
693+
if usg.IOBlockSize != nil && usage.IOBlockSize != nil {
694+
*usage.IOBlockSize = min(*usage.IOBlockSize, *usg.IOBlockSize)
695+
} else {
696+
usage.IOBlockSize = nil
697+
}
698+
}
699+
// If no upstream provided IOBlockSize, set to nil
700+
if usage.IOBlockSize != nil && *usage.IOBlockSize == 0 {
701+
usage.IOBlockSize = nil
692702
}
693703
return usage, nil
694704
}

cmd/cmount/fs.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,9 @@ func (fsys *FS) getNode(path string, fh uint64) (node vfs.Node, handle vfs.Handl
138138

139139
// stat fills up the stat block for Node
140140
func (fsys *FS) stat(node vfs.Node, stat *fuse.Stat_t) (errc int) {
141-
Size := uint64(node.Size())
142-
Blocks := (Size + 511) / 512
141+
size := uint64(node.Size())
142+
dataBlockSize, ioBlockSize := fsys.VFS.GetBlockSizes()
143+
blocks := (size + uint64(dataBlockSize) - 1) / uint64(dataBlockSize)
143144
modTime := node.ModTime()
144145
//stat.Dev = 1
145146
stat.Ino = node.Inode() // FIXME do we need to set the inode number?
@@ -148,13 +149,13 @@ func (fsys *FS) stat(node vfs.Node, stat *fuse.Stat_t) (errc int) {
148149
stat.Uid = fsys.VFS.Opt.UID
149150
stat.Gid = fsys.VFS.Opt.GID
150151
//stat.Rdev
151-
stat.Size = int64(Size)
152+
stat.Size = int64(size)
152153
t := fuse.NewTimespec(modTime)
153154
stat.Atim = t
154155
stat.Mtim = t
155156
stat.Ctim = t
156-
stat.Blksize = 512
157-
stat.Blocks = int64(Blocks)
157+
stat.Blocks = int64(blocks)
158+
stat.Blksize = int64(ioBlockSize)
158159
stat.Birthtim = t
159160
// fs.Debugf(nil, "stat = %+v", *stat)
160161
return 0
@@ -258,16 +259,17 @@ func (fsys *FS) Releasedir(path string, fh uint64) (errc int) {
258259
// Statfs reads overall stats on the filesystem
259260
func (fsys *FS) Statfs(path string, stat *fuse.Statfs_t) (errc int) {
260261
defer log.Trace(path, "")("stat=%+v, errc=%d", stat, &errc)
261-
const blockSize = 4096
262+
_, ioBlockSize := fsys.VFS.GetBlockSizes()
263+
blockSize := uint64(ioBlockSize)
262264
total, _, free := fsys.VFS.Statfs()
263265
stat.Blocks = uint64(total) / blockSize // Total data blocks in file system.
264266
stat.Bfree = uint64(free) / blockSize // Free blocks in file system.
265267
stat.Bavail = stat.Bfree // Free blocks in file system if you're not root.
266268
stat.Files = 1e9 // Total files in file system.
267269
stat.Ffree = 1e9 // Free files in file system.
268-
stat.Bsize = blockSize // Block size
270+
stat.Bsize = uint64(blockSize) // Block size
269271
stat.Namemax = 255 // Maximum file name length?
270-
stat.Frsize = blockSize // Fragment size, smallest addressable data size in the file system.
272+
stat.Frsize = uint64(blockSize) // Fragment size, smallest addressable data size in the file system.
271273
mountlib.ClipBlocks(&stat.Blocks)
272274
mountlib.ClipBlocks(&stat.Bfree)
273275
mountlib.ClipBlocks(&stat.Bavail)

cmd/mount/file.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,18 @@ func (f *File) Attr(ctx context.Context, a *fuse.Attr) (err error) {
2828
defer log.Trace(f, "")("a=%+v, err=%v", a, &err)
2929
a.Valid = time.Duration(f.fsys.opt.AttrTimeout)
3030
modTime := f.File.ModTime()
31-
Size := uint64(f.File.Size())
32-
Blocks := (Size + 511) / 512
31+
size := uint64(f.File.Size())
32+
dataBlockSize, ioBlockSize := f.VFS().GetBlockSizes()
33+
blocks := (size + uint64(dataBlockSize) - 1) / uint64(dataBlockSize)
3334
a.Gid = f.VFS().Opt.GID
3435
a.Uid = f.VFS().Opt.UID
3536
a.Mode = f.File.Mode() &^ os.ModeAppend
36-
a.Size = Size
37+
a.Size = size
3738
a.Atime = modTime
3839
a.Mtime = modTime
3940
a.Ctime = modTime
40-
a.Blocks = Blocks
41+
a.Blocks = blocks
42+
a.BlockSize = uint32(ioBlockSize)
4143
return nil
4244
}
4345

cmd/mount/fs.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,17 @@ var _ fusefs.FSStatfser = (*FS)(nil)
5555
// It should write that data to resp.
5656
func (f *FS) Statfs(ctx context.Context, req *fuse.StatfsRequest, resp *fuse.StatfsResponse) (err error) {
5757
defer log.Trace("", "")("stat=%+v, err=%v", resp, &err)
58-
const blockSize = 4096
58+
_, ioBlockSize := f.VFS.GetBlockSizes()
59+
blockSize := uint64(ioBlockSize)
5960
total, _, free := f.VFS.Statfs()
6061
resp.Blocks = uint64(total) / blockSize // Total data blocks in file system.
6162
resp.Bfree = uint64(free) / blockSize // Free blocks in file system.
6263
resp.Bavail = resp.Bfree // Free blocks in file system if you're not root.
6364
resp.Files = 1e9 // Total files in file system.
6465
resp.Ffree = 1e9 // Free files in file system.
65-
resp.Bsize = blockSize // Block size
66+
resp.Bsize = uint32(blockSize) // Block size
6667
resp.Namelen = 255 // Maximum file name length?
67-
resp.Frsize = blockSize // Fragment size, smallest addressable data size in the file system.
68+
resp.Frsize = uint32(blockSize) // Fragment size, smallest addressable data size in the file system.
6869
mountlib.ClipBlocks(&resp.Blocks)
6970
mountlib.ClipBlocks(&resp.Bfree)
7071
mountlib.ClipBlocks(&resp.Bavail)

cmd/mount2/fs.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,20 @@ func getMode(node os.FileInfo) uint32 {
8787
}
8888

8989
// fill in attr from node
90-
func setAttr(node vfs.Node, attr *fuse.Attr) {
91-
Size := uint64(node.Size())
92-
const BlockSize = 512
93-
Blocks := (Size + BlockSize - 1) / BlockSize
90+
func (f *FS) setAttr(node vfs.Node, attr *fuse.Attr) {
91+
size := uint64(node.Size())
92+
vfs := node.VFS()
93+
dataBlockSize, ioBlockSize := vfs.GetBlockSizes()
94+
blocks := (size + uint64(dataBlockSize) - 1) / uint64(dataBlockSize)
9495
modTime := node.ModTime()
9596
// set attributes
96-
vfs := node.VFS()
9797
attr.Owner.Gid = vfs.Opt.GID
9898
attr.Owner.Uid = vfs.Opt.UID
9999
attr.Mode = getMode(node)
100-
attr.Size = Size
100+
attr.Size = size
101101
attr.Nlink = 1
102-
attr.Blocks = Blocks
103-
// attr.Blksize = BlockSize // not supported in freebsd/darwin, defaults to 4k if not set
102+
attr.Blocks = blocks
103+
attr.Blksize = uint32(ioBlockSize)
104104
s := uint64(modTime.Unix())
105105
ns := uint32(modTime.Nanosecond())
106106
attr.Atime = s
@@ -114,13 +114,13 @@ func setAttr(node vfs.Node, attr *fuse.Attr) {
114114

115115
// fill in AttrOut from node
116116
func (f *FS) setAttrOut(node vfs.Node, out *fuse.AttrOut) {
117-
setAttr(node, &out.Attr)
117+
f.setAttr(node, &out.Attr)
118118
out.SetTimeout(time.Duration(f.opt.AttrTimeout))
119119
}
120120

121121
// fill in EntryOut from node
122122
func (f *FS) setEntryOut(node vfs.Node, out *fuse.EntryOut) {
123-
setAttr(node, &out.Attr)
123+
f.setAttr(node, &out.Attr)
124124
out.SetEntryTimeout(time.Duration(f.opt.AttrTimeout))
125125
out.SetAttrTimeout(time.Duration(f.opt.AttrTimeout))
126126
}

cmd/mount2/node.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,17 @@ func (n *Node) lookupDir(leaf string) (*vfs.Dir, syscall.Errno) {
101101
// will not work.
102102
func (n *Node) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno {
103103
defer log.Trace(n, "")("out=%+v", &out)
104-
const blockSize = 4096
105104
total, _, free := n.VFS().Statfs()
105+
_, ioBlockSize := n.VFS().GetBlockSizes()
106+
blockSize := uint64(ioBlockSize)
106107
out.Blocks = uint64(total) / blockSize // Total data blocks in file system.
107108
out.Bfree = uint64(free) / blockSize // Free blocks in file system.
108109
out.Bavail = out.Bfree // Free blocks in file system if you're not root.
109110
out.Files = 1e9 // Total files in file system.
110111
out.Ffree = 1e9 // Free files in file system.
111-
out.Bsize = blockSize // Block size
112+
out.Bsize = uint32(blockSize) // Block size
112113
out.NameLen = 255 // Maximum file name length?
113-
out.Frsize = blockSize // Fragment size, smallest addressable data size in the file system.
114+
out.Frsize = uint32(blockSize) // Fragment size, smallest addressable data size in the file system.
114115
mountlib.ClipBlocks(&out.Blocks)
115116
mountlib.ClipBlocks(&out.Bfree)
116117
mountlib.ClipBlocks(&out.Bavail)

fs/types.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -365,16 +365,32 @@ func NewUsageValue[T interface {
365365
return p
366366
}
367367

368+
// NewUsageValue32 makes a valid int32 value
369+
func NewUsageValue32[T interface {
370+
int64 | int32 | int
371+
}](value T) *int32 {
372+
p := new(int32)
373+
if value > T(int32(math.MaxInt32)) {
374+
*p = math.MaxInt32
375+
} else if value < 0 {
376+
*p = 0
377+
} else {
378+
*p = int32(value)
379+
}
380+
return p
381+
}
382+
368383
// Usage is returned by the About call
369384
//
370385
// If a value is nil then it isn't supported by that backend
371386
type Usage struct {
372-
Total *int64 `json:"total,omitempty"` // quota of bytes that can be used
373-
Used *int64 `json:"used,omitempty"` // bytes in use
374-
Trashed *int64 `json:"trashed,omitempty"` // bytes in trash
375-
Other *int64 `json:"other,omitempty"` // other usage e.g. gmail in drive
376-
Free *int64 `json:"free,omitempty"` // bytes which can be uploaded before reaching the quota
377-
Objects *int64 `json:"objects,omitempty"` // objects in the storage system
387+
Total *int64 `json:"total,omitempty"` // quota of bytes that can be used
388+
Used *int64 `json:"used,omitempty"` // bytes in use
389+
Trashed *int64 `json:"trashed,omitempty"` // bytes in trash
390+
Other *int64 `json:"other,omitempty"` // other usage e.g. gmail in drive
391+
Free *int64 `json:"free,omitempty"` // bytes which can be uploaded before reaching the quota
392+
Objects *int64 `json:"objects,omitempty"` // objects in the storage system
393+
IOBlockSize *int32 `json:"ioblocksize,omitempty"` // preferred IO block size for this filesystem
378394
}
379395

380396
// WriterAtCloser wraps io.WriterAt and io.Closer

vfs/vfs.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,28 @@ func (vfs *VFS) Statfs() (total, used, free int64) {
702702
return
703703
}
704704

705+
// GetBlockSizes returns both the data block size and IO block size.
706+
// DataBlockSize is always 512 for POSIX compatibility.
707+
// IOBlockSize is obtained from the filesystem via About() call, or 4096 as default.
708+
func (vfs *VFS) GetBlockSizes() (int32, int32) {
709+
const dataBlockSize = int32(512)
710+
711+
// Call Statfs to ensure we have up-to-date usage information
712+
_, _, _ = vfs.Statfs()
713+
714+
vfs.usageMu.Lock()
715+
defer vfs.usageMu.Unlock()
716+
717+
var ioBlockSize int32 = 4096 // Default
718+
if vfs.usage != nil && vfs.usage.IOBlockSize != nil {
719+
if *vfs.usage.IOBlockSize > 0 {
720+
ioBlockSize = *vfs.usage.IOBlockSize
721+
}
722+
}
723+
724+
return dataBlockSize, ioBlockSize
725+
}
726+
705727
// Remove removes the named file or (empty) directory.
706728
func (vfs *VFS) Remove(name string) error {
707729
node, err := vfs.Stat(name)

0 commit comments

Comments
 (0)