diff options
| author | Jakob Unterwurzacher | 2020-10-18 00:25:42 +0200 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2020-10-18 00:25:42 +0200 | 
| commit | c943ed32aaf94a4e60d96c7a513180d29b15a40a (patch) | |
| tree | e64fc1672ccb57abb71327b38640eca79a3adaf8 /internal/syscallcompat | |
| parent | 95ea7383f9aeef83e6ce7a06d49d0f24857ef30a (diff) | |
syscallcompat: add getxattr fastpaths
The allocations from Lgetxattr were #1 in the tar extract
allocation profile (caused by security.capability lookups).
No more!
$ benchstat old.txt new.txt
name         old time/op  new time/op  delta
Lgetxattr-4  15.2µs ± 0%   1.8µs ± 0%   ~     (p=1.000 n=1+1)
$ ./benchmark.bash
Testing gocryptfs at /tmp/benchmark.bash.H8p: gocryptfs v2.0-beta1-4-g95ea738-dirty; go-fuse v2.0.4-0.20200908172753-0b6cbc515082 => github.com/rfjakob/go-fuse/v2 v2.0.4-0.20201015204057-88b12c99f8af; 2020-10-18 go1.15.3 linux/amd64
/tmp/benchmark.bash.H8p.mnt is a mountpoint
WRITE: 262144000 bytes (262 MB, 250 MiB) copied, 0,520109 s, 504 MB/s
READ:  262144000 bytes (262 MB, 250 MiB) copied, 0,255672 s, 1,0 GB/s
UNTAR: 30,238
MD5:   12,721
LS:    10,038
RM:    16,536
Diffstat (limited to 'internal/syscallcompat')
| -rw-r--r-- | internal/syscallcompat/sys_common.go | 73 | ||||
| -rw-r--r-- | internal/syscallcompat/sys_common_test.go | 2 | 
2 files changed, 43 insertions, 32 deletions
| diff --git a/internal/syscallcompat/sys_common.go b/internal/syscallcompat/sys_common.go index d178a9b..d7a9706 100644 --- a/internal/syscallcompat/sys_common.go +++ b/internal/syscallcompat/sys_common.go @@ -138,11 +138,47 @@ func Fstatat2(dirfd int, path string, flags int) (*syscall.Stat_t, error) {  const XATTR_SIZE_MAX = 65536 -// Make the buffer 1kB bigger so we can detect overflows +// Make the buffer 1kB bigger so we can detect overflows. Unfortunately, +// slices larger than 64kB are always allocated on the heap.  const XATTR_BUFSZ = XATTR_SIZE_MAX + 1024 +// We try with a small buffer first - this one can be allocated on the stack. +const XATTR_BUFSZ_SMALL = 500 +  // Fgetxattr is a wrapper around unix.Fgetxattr that handles the buffer sizing.  func Fgetxattr(fd int, attr string) (val []byte, err error) { +	fn := func(buf []byte) (int, error) { +		return unix.Fgetxattr(fd, attr, buf) +	} +	return getxattrSmartBuf(fn) +} + +// Lgetxattr is a wrapper around unix.Lgetxattr that handles the buffer sizing. +func Lgetxattr(path string, attr string) (val []byte, err error) { +	fn := func(buf []byte) (int, error) { +		return unix.Lgetxattr(path, attr, buf) +	} +	return getxattrSmartBuf(fn) +} + +func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) { +	// Fastpaths. Important for security.capabilities, which gets queried a lot. +	buf := make([]byte, XATTR_BUFSZ_SMALL) +	sz, err := fn(buf) +	// Non-existing xattr +	if err == unix.ENODATA { +		return nil, err +	} +	// Underlying fs does not support security.capabilities (example: tmpfs) +	if err == unix.EOPNOTSUPP { +		return nil, err +	} +	// Small xattr +	if err == nil && sz < len(buf) { +		goto out +	} +	// Generic slowpath +	//  	// If the buffer is too small to fit the value, Linux and MacOS react  	// differently:  	// Linux: returns an ERANGE error and "-1" bytes. @@ -151,34 +187,8 @@ func Fgetxattr(fd int, attr string) (val []byte, err error) {  	// We choose the simple approach of buffer that is bigger than the limit on  	// Linux, and return an error for everything that is bigger (which can  	// only happen on MacOS). -	// -	// See https://github.com/pkg/xattr for a smarter solution. -	// TODO: smarter buffer sizing? -	buf := make([]byte, XATTR_BUFSZ) -	sz, err := unix.Fgetxattr(fd, attr, buf) -	if err == syscall.ERANGE { -		// Do NOT return ERANGE - the user might retry ad inifinitum! -		return nil, syscall.EOVERFLOW -	} -	if err != nil { -		return nil, err -	} -	if sz >= XATTR_SIZE_MAX { -		return nil, syscall.EOVERFLOW -	} -	// Copy only the actually used bytes to a new (smaller) buffer -	// so "buf" never leaves the function and can be allocated on the stack. -	val = make([]byte, sz) -	copy(val, buf) -	return val, nil -} - -// Lgetxattr is a wrapper around unix.Lgetxattr that handles the buffer sizing. -func Lgetxattr(path string, attr string) (val []byte, err error) { -	// See the buffer sizing comments in Fgetxattr. -	// TODO: smarter buffer sizing? -	buf := make([]byte, XATTR_BUFSZ) -	sz, err := unix.Lgetxattr(path, attr, buf) +	buf = make([]byte, XATTR_BUFSZ) +	sz, err = fn(buf)  	if err == syscall.ERANGE {  		// Do NOT return ERANGE - the user might retry ad inifinitum!  		return nil, syscall.EOVERFLOW @@ -189,9 +199,10 @@ func Lgetxattr(path string, attr string) (val []byte, err error) {  	if sz >= XATTR_SIZE_MAX {  		return nil, syscall.EOVERFLOW  	} +out:  	// Copy only the actually used bytes to a new (smaller) buffer  	// so "buf" never leaves the function and can be allocated on the stack. -	val = make([]byte, sz) +	val := make([]byte, sz)  	copy(val, buf)  	return val, nil  } @@ -199,7 +210,7 @@ func Lgetxattr(path string, attr string) (val []byte, err error) {  // Flistxattr is a wrapper for unix.Flistxattr that handles buffer sizing and  // parsing the returned blob to a string slice.  func Flistxattr(fd int) (attrs []string, err error) { -	// See the buffer sizing comments in Fgetxattr. +	// See the buffer sizing comments in getxattrSmartBuf.  	// TODO: smarter buffer sizing?  	buf := make([]byte, XATTR_BUFSZ)  	sz, err := unix.Flistxattr(fd, buf) diff --git a/internal/syscallcompat/sys_common_test.go b/internal/syscallcompat/sys_common_test.go index f53b0d2..c5f05a9 100644 --- a/internal/syscallcompat/sys_common_test.go +++ b/internal/syscallcompat/sys_common_test.go @@ -318,6 +318,6 @@ func TestFstatat(t *testing.T) {  // queries security.capabilities for every file access.  func BenchmarkLgetxattr(b *testing.B) {  	for i := 0; i < b.N; i++ { -		Lgetxattr("/", "this.attr.does.not.exist") +		Lgetxattr("/", "user.this.attr.does.not.exist")  	}  } | 
