Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

memfs: Add thread-safety to Memory #110

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open

memfs: Add thread-safety to Memory #110

wants to merge 5 commits into from

Conversation

pjbgf
Copy link
Member

@pjbgf pjbgf commented Jan 27, 2025

Make Memory thread-safe for operations such as Create, Rename and Delete. When compared with Memory without a mutex, the cost per op is roughly 50ns, as per benchmark below:

goos: linux
goarch: amd64
pkg: github.com/go-git/go-billy/v6/test
cpu: AMD Ryzen 7 PRO 8840HS w/ Radeon 780M Graphics
BenchmarkCompare/stdlib_open-16         	  341277	      3303 ns/op	     152 B/op	       3 allocs/op
BenchmarkCompare/osfs.chrootOS_open-16  	  286580	      3806 ns/op	     360 B/op	       9 allocs/op
BenchmarkCompare/osfs.boundOS_open-16   	  209929	      5311 ns/op	     984 B/op	      20 allocs/op
BenchmarkCompare/memfs_open-16          	 1503024	       967.4 ns/op	     224 B/op	      10 allocs/op
BenchmarkCompare/memfs_mutex_open-16    	 1272890	       911.6 ns/op	     224 B/op	      10 allocs/op
BenchmarkCompare/stdlib_read-16         	  184956	      5957 ns/op	 171.88 MB/s	       0 B/op	       0 allocs/op
BenchmarkCompare/osfs.chrootOS_read-16  	  180552	      6207 ns/op	 164.97 MB/s	       0 B/op	       0 allocs/op
BenchmarkCompare/osfs.boundOS_read-16   	  179314	      6131 ns/op	 167.02 MB/s	       0 B/op	       0 allocs/op
BenchmarkCompare/memfs_read-16          	  792040	      1568 ns/op	 653.02 MB/s	       0 B/op	       0 allocs/op
BenchmarkCompare/memfs_mutex_read-16    	  783422	      1609 ns/op	 636.27 MB/s	       0 B/op	       0 allocs/op
BenchmarkCompare/stdlib_create-16       	  216717	      5232 ns/op	     152 B/op	       3 allocs/op
BenchmarkCompare/osfs.chrootOS_create-16         	  157268	      7017 ns/op	     616 B/op	      11 allocs/op
BenchmarkCompare/osfs.boundOS_create-16          	  123038	      9242 ns/op	    1335 B/op	      22 allocs/op
BenchmarkCompare/memfs_create-16                 	  880813	      1954 ns/op	     693 B/op	      12 allocs/op
BenchmarkCompare/memfs_mutex_create-16           	  559534	      1901 ns/op	     631 B/op	      12 allocs/op
PASS
ok  	github.com/go-git/go-billy/v6/test	118.368s

Some minor changes to Rename were made on osfs implementations, to ensure consistent behaviour across the built-in
implementations.

Additional thread-safety will be required for memfs.file, which will be dealt with separately.

Closes #40.
Relatest to go-git/go-git#773.

PS: The benchmark results were based before the removal of the Mutex option (last commit).

memfs/memory.go Outdated
@@ -24,11 +22,25 @@ const separator = filepath.Separator
// Memory a very convenient filesystem based on memory files.
type Memory struct {
s *storage

m mutex
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not sync.RWMutext?

Suggested change
m mutex
m sync.RWMutext

memfs/memory.go Outdated
Comment on lines 31 to 34
o := &options{
// Enable thread-safety by default.
newMutex: newMutex,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should always enable thread-safety

pjbgf added 4 commits January 28, 2025 17:38
This benchmark aims to compare the time and space complexity across the
different billy implementations, which will later be used to assess their
trends over time.

The initial figures shows that there is a substantial difference in cost
for the Open operation amongst the os-based options, however the impact on
Read operations is more subtle. As expected, in memory has the best figures
for most cases.

goos: linux
goarch: amd64
pkg: github.com/go-git/go-billy/v6/test
cpu: AMD Ryzen 7 PRO 8840HS w/ Radeon 780M Graphics
BenchmarkOpenRead/stdlib_open-16         	  319782	      3750 ns/op	     152 B/op	       3 allocs/op
BenchmarkOpenRead/osfs.chrootOS_open-16  	  248833	      4620 ns/op	     360 B/op	       9 allocs/op
BenchmarkOpenRead/osfs.boundOS_open-16   	  182808	      6370 ns/op	     984 B/op	      20 allocs/op
BenchmarkOpenRead/memfs_open-16          	 1000000	      1038 ns/op	     224 B/op	      10 allocs/op
BenchmarkOpenRead/stdlib_read-16         	  165465	      6791 ns/op	 150.79 MB/s	       0 B/op	       0 allocs/op
BenchmarkOpenRead/osfs.chrootOS_read-16  	  158107	      7018 ns/op	 145.90 MB/s	       0 B/op	       0 allocs/op
BenchmarkOpenRead/osfs.boundOS_read-16   	  155649	      6865 ns/op	 149.15 MB/s	       0 B/op	       0 allocs/op
BenchmarkOpenRead/memfs_read-16          	  705140	      1703 ns/op	 601.17 MB/s	       0 B/op	       0 allocs/op
PASS
ok  	github.com/go-git/go-billy/v6/test	34.080s

Signed-off-by: Paulo Gomes <[email protected]>
Ahead of making changes to Rename, ensures that the behaviour across the built-in
implementations are aligned by expanding tests, so that we avoid future regression.

Signed-off-by: Paulo Gomes <[email protected]>
Make Memory thread safe for operations such as Create, Rename and Delete.
When compared with Memory without a mutex, the cost per op is roughly 50ns,
as per benchmark below:

goos: linux
goarch: amd64
pkg: github.com/go-git/go-billy/v6/test
cpu: AMD Ryzen 7 PRO 8840HS w/ Radeon 780M Graphics
BenchmarkCompare/stdlib_open-16         	  341277	      3303 ns/op	     152 B/op	       3 allocs/op
BenchmarkCompare/osfs.chrootOS_open-16  	  286580	      3806 ns/op	     360 B/op	       9 allocs/op
BenchmarkCompare/osfs.boundOS_open-16   	  209929	      5311 ns/op	     984 B/op	      20 allocs/op
BenchmarkCompare/memfs_open-16          	 1503024	       967.4 ns/op	     224 B/op	      10 allocs/op
BenchmarkCompare/memfs_mutex_open-16    	 1272890	       911.6 ns/op	     224 B/op	      10 allocs/op
BenchmarkCompare/stdlib_read-16         	  184956	      5957 ns/op	 171.88 MB/s	       0 B/op	       0 allocs/op
BenchmarkCompare/osfs.chrootOS_read-16  	  180552	      6207 ns/op	 164.97 MB/s	       0 B/op	       0 allocs/op
BenchmarkCompare/osfs.boundOS_read-16   	  179314	      6131 ns/op	 167.02 MB/s	       0 B/op	       0 allocs/op
BenchmarkCompare/memfs_read-16          	  792040	      1568 ns/op	 653.02 MB/s	       0 B/op	       0 allocs/op
BenchmarkCompare/memfs_mutex_read-16    	  783422	      1609 ns/op	 636.27 MB/s	       0 B/op	       0 allocs/op
BenchmarkCompare/stdlib_create-16       	  216717	      5232 ns/op	     152 B/op	       3 allocs/op
BenchmarkCompare/osfs.chrootOS_create-16         	  157268	      7017 ns/op	     616 B/op	      11 allocs/op
BenchmarkCompare/osfs.boundOS_create-16          	  123038	      9242 ns/op	    1335 B/op	      22 allocs/op
BenchmarkCompare/memfs_create-16                 	  880813	      1954 ns/op	     693 B/op	      12 allocs/op
BenchmarkCompare/memfs_mutex_create-16           	  559534	      1901 ns/op	     631 B/op	      12 allocs/op
PASS
ok  	github.com/go-git/go-billy/v6/test	118.368s

This change enables users to opt-out (memfs.WithoutMutex), in case speed
on a single thread is more important than being thread-safe.

Additional thread-safety will be required for memfs.file.

Signed-off-by: Paulo Gomes <[email protected]>
memfs/memory.go Outdated
Comment on lines 31 to 35
func New(opts ...Option) billy.Filesystem {
o := &options{
// Enable thread-safety by default.
newMutex: newMutex,
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need opts ...Option and options?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the plan is to align with the other osfs implementations in a future PR.

memfs/memory.go Outdated

fs := &Memory{
s: newStorage(),
m: sync.RWMutex{},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be omitted

Suggested change
m: sync.RWMutex{},

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout, let me amend that.

Comment on lines +1 to +6
package memfs

type Option func(*options)

type options struct {
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this altogether

Suggested change
package memfs
type Option func(*options)
type options struct {
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this on purpose, the idea is to add additional options on a follow-up PR. Please note that the pattern aligns with both osfs implementations.

Comment on lines +31 to +32
func New(opts ...Option) billy.Filesystem {
o := &options{}
for _, opt := range opts {
opt(o)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func New(opts ...Option) billy.Filesystem {
o := &options{}
for _, opt := range opts {
opt(o)
}
func New() billy.Filesystem {

Ensure memfs is thread-safe at all times. In case the single thread
performance is more important (probably outside of go-git) that can
be fixed with future optimisation.

In the context of go-git, this should not be a concern as there are
other bottlenecks (e.g. sha1cd).

Signed-off-by: Paulo Gomes <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Creating new files is not multi-goroutine safe
2 participants