Skip to content

Commit 90d01bc

Browse files
committed
dirent: no longer cache the results of Stat and Info
This commit changes fastwalk.DirEntry to no longer cache the result of calling the Stat and Info methods.
1 parent f8029ee commit 90d01bc

File tree

6 files changed

+45
-226
lines changed

6 files changed

+45
-226
lines changed

dirent.go

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,26 @@ package fastwalk
33
import (
44
"io/fs"
55
"os"
6-
"sync"
7-
"sync/atomic"
86
"syscall"
9-
"unsafe"
107
)
118

12-
type fileInfo struct {
13-
once sync.Once
14-
fs.FileInfo
15-
err error
16-
}
17-
18-
func loadFileInfo(pinfo **fileInfo) *fileInfo {
19-
ptr := (*unsafe.Pointer)(unsafe.Pointer(pinfo))
20-
fi := (*fileInfo)(atomic.LoadPointer(ptr))
21-
if fi == nil {
22-
fi = &fileInfo{}
23-
if !atomic.CompareAndSwapPointer(
24-
(*unsafe.Pointer)(unsafe.Pointer(pinfo)), // adrr
25-
nil, // old
26-
unsafe.Pointer(fi), // new
27-
) {
28-
fi = (*fileInfo)(atomic.LoadPointer(ptr))
29-
}
30-
}
31-
return fi
32-
}
33-
34-
// StatDirEntry returns a [fs.FileInfo] describing the named file ([os.Stat]).
35-
// If de is a [fastwalk.DirEntry] its Stat method is used and the returned
36-
// FileInfo may be cached from a prior call to Stat. If a cached result is not
37-
// desired, users should just call [os.Stat] directly.
9+
// StatDirEntry returns a [fs.FileInfo] describing the named file. If de is a
10+
// [fastwalk.DirEntry] its Stat method is used. Otherwise, [os.Stat] is called
11+
// on the path. Therefore, de should be the DirEntry describing path.
3812
//
39-
// This is a helper function for calling Stat on the DirEntry passed to the
40-
// walkFn argument to [Walk].
13+
// Note: This function was added when fastwalk used to always cache the result
14+
// of DirEntry.Stat. Now that fastwalk no longer explicitly caches the result
15+
// of Stat this function is slightly less useful and is equivalent to the below
16+
// code:
4117
//
42-
// The path argument is only used if de is not of type [fastwalk.DirEntry].
43-
// Therefore, de should be the DirEntry describing path.
18+
// if d, ok := de.(DirEntry); ok {
19+
// return d.Stat()
20+
// }
21+
// return os.Stat(path)
4422
func StatDirEntry(path string, de fs.DirEntry) (fs.FileInfo, error) {
4523
if de == nil {
4624
return nil, &os.PathError{Op: "stat", Path: path, Err: syscall.EINVAL}
4725
}
48-
if de.Type()&os.ModeSymlink == 0 {
49-
return de.Info()
50-
}
5126
if d, ok := de.(DirEntry); ok {
5227
return d.Stat()
5328
}

dirent_portable.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package fastwalk
88
import (
99
"io/fs"
1010
"os"
11+
"runtime"
1112
"sort"
1213
"sync"
1314

@@ -19,7 +20,6 @@ var _ DirEntry = (*portableDirent)(nil)
1920
type portableDirent struct {
2021
fs.DirEntry
2122
parent string
22-
stat *fileInfo
2323
depth uint32
2424
}
2525

@@ -32,14 +32,14 @@ func (d *portableDirent) Depth() int {
3232
}
3333

3434
func (d *portableDirent) Stat() (fs.FileInfo, error) {
35-
if d.DirEntry.Type()&os.ModeSymlink == 0 {
36-
return d.DirEntry.Info()
35+
if runtime.GOOS == "windows" {
36+
// On Windows use Info() if the file is not a symlink since
37+
// IIRC the result is cached when the FileInfo is created.
38+
if d.DirEntry.Type()&fs.ModeSymlink != 0 {
39+
return d.Info()
40+
}
3741
}
38-
stat := loadFileInfo(&d.stat)
39-
stat.once.Do(func() {
40-
stat.FileInfo, stat.err = os.Stat(d.parent + string(os.PathSeparator) + d.Name())
41-
})
42-
return stat.FileInfo, stat.err
42+
return os.Stat(d.parent + string(os.PathSeparator) + d.Name())
4343
}
4444

4545
func newDirEntry(dirName string, info fs.DirEntry, depth int) DirEntry {

dirent_test.go

Lines changed: 0 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import (
66
"io/fs"
77
"os"
88
"path/filepath"
9-
"runtime"
10-
"sync"
119
"testing"
1210

1311
"github.com/charlievieth/fastwalk"
@@ -75,91 +73,5 @@ func TestDirent(t *testing.T) {
7573
t.Errorf("lstat mismatch\n got:\n%s\nwant:\n%s",
7674
fastwalk.FormatFileInfo(got), fastwalk.FormatFileInfo(want))
7775
}
78-
fi, err := fileEnt.Info()
79-
if err != nil {
80-
t.Fatal(err)
81-
}
82-
if fi != got {
83-
t.Error("failed to return or cache FileInfo")
84-
}
85-
de := fileEnt.(fastwalk.DirEntry)
86-
fi, err = de.Stat()
87-
if err != nil {
88-
t.Fatal(err)
89-
}
90-
if fi != got {
91-
t.Error("failed to use cached Info result for non-symlink")
92-
}
93-
})
94-
95-
t.Run("Parallel", func(t *testing.T) {
96-
testParallel := func(t *testing.T, de fs.DirEntry, fn func() (fs.FileInfo, error)) {
97-
numCPU := runtime.NumCPU()
98-
99-
infos := make([][]fs.FileInfo, numCPU)
100-
for i := range infos {
101-
infos[i] = make([]fs.FileInfo, 100)
102-
}
103-
104-
// Start all the goroutines at the same time to
105-
// maximise the chance of a race
106-
start := make(chan struct{})
107-
var wg, ready sync.WaitGroup
108-
ready.Add(numCPU)
109-
wg.Add(numCPU)
110-
for i := 0; i < numCPU; i++ {
111-
go func(fis []fs.FileInfo, de fs.DirEntry) {
112-
defer wg.Done()
113-
ready.Done()
114-
<-start
115-
for i := range fis {
116-
fis[i], _ = de.Info()
117-
}
118-
}(infos[i], de)
119-
}
120-
121-
ready.Wait()
122-
close(start) // start all goroutines at once
123-
wg.Wait()
124-
125-
first := infos[0][0]
126-
if first == nil {
127-
t.Fatal("failed to stat file:", de.Name())
128-
}
129-
for _, fis := range infos {
130-
for _, fi := range fis {
131-
if fi != first {
132-
t.Errorf("Expected the same fs.FileInfo to always "+
133-
"be returned got: %#v want: %#v", fi, first)
134-
}
135-
}
136-
}
137-
}
138-
139-
t.Run("File", func(t *testing.T) {
140-
t.Run("Stat", func(t *testing.T) {
141-
_, fileEnt := getDirEnts(t)
142-
de := fileEnt.(fastwalk.DirEntry)
143-
testParallel(t, de, de.Stat)
144-
})
145-
t.Run("Lstat", func(t *testing.T) {
146-
_, fileEnt := getDirEnts(t)
147-
de := fileEnt.(fastwalk.DirEntry)
148-
testParallel(t, de, de.Info)
149-
})
150-
})
151-
152-
t.Run("Link", func(t *testing.T) {
153-
t.Run("Stat", func(t *testing.T) {
154-
linkEnt, _ := getDirEnts(t)
155-
de := linkEnt.(fastwalk.DirEntry)
156-
testParallel(t, de, de.Stat)
157-
})
158-
t.Run("Lstat", func(t *testing.T) {
159-
linkEnt, _ := getDirEnts(t)
160-
de := linkEnt.(fastwalk.DirEntry)
161-
testParallel(t, de, de.Info)
162-
})
163-
})
16476
})
16577
}

dirent_unix.go

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ type unixDirent struct {
1616
name string
1717
typ fs.FileMode
1818
depth uint32 // uint32 so that we can pack it next to typ
19-
info *fileInfo
20-
stat *fileInfo
2119
}
2220

2321
func (d *unixDirent) Name() string { return d.name }
@@ -27,22 +25,11 @@ func (d *unixDirent) Depth() int { return int(d.depth) }
2725
func (d *unixDirent) String() string { return fmtdirent.FormatDirEntry(d) }
2826

2927
func (d *unixDirent) Info() (fs.FileInfo, error) {
30-
info := loadFileInfo(&d.info)
31-
info.once.Do(func() {
32-
info.FileInfo, info.err = os.Lstat(d.parent + "/" + d.name)
33-
})
34-
return info.FileInfo, info.err
28+
return os.Lstat(d.parent + "/" + d.name)
3529
}
3630

3731
func (d *unixDirent) Stat() (fs.FileInfo, error) {
38-
if d.typ&os.ModeSymlink == 0 {
39-
return d.Info()
40-
}
41-
stat := loadFileInfo(&d.stat)
42-
stat.once.Do(func() {
43-
stat.FileInfo, stat.err = os.Stat(d.parent + "/" + d.name)
44-
})
45-
return stat.FileInfo, stat.err
32+
return os.Stat(d.parent + "/" + d.name)
4633
}
4734

4835
func newUnixDirent(parent, name string, typ fs.FileMode, depth int) *unixDirent {
@@ -55,16 +42,7 @@ func newUnixDirent(parent, name string, typ fs.FileMode, depth int) *unixDirent
5542
}
5643

5744
func fileInfoToDirEntry(dirname string, fi fs.FileInfo) DirEntry {
58-
info := &fileInfo{
59-
FileInfo: fi,
60-
}
61-
info.once.Do(func() {})
62-
return &unixDirent{
63-
parent: dirname,
64-
name: fi.Name(),
65-
typ: fi.Mode().Type(),
66-
info: info,
67-
}
45+
return newUnixDirent(dirname, fi.Name(), fi.Mode().Type(), 0)
6846
}
6947

7048
var direntSlicePool = sync.Pool{

dirent_unix_test.go

Lines changed: 20 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ import (
1010
"reflect"
1111
"runtime"
1212
"sync"
13-
"sync/atomic"
1413
"testing"
1514
"time"
16-
"unsafe"
1715
)
1816

1917
func testUnixDirentParallel(t *testing.T, ent *unixDirent, want fs.FileInfo,
@@ -23,7 +21,7 @@ func testUnixDirentParallel(t *testing.T, ent *unixDirent, want fs.FileInfo,
2321
return fi1.Name() == fi2.Name() &&
2422
fi1.Size() == fi2.Size() &&
2523
fi1.Mode() == fi2.Mode() &&
26-
fi1.ModTime() == fi2.ModTime() &&
24+
fi1.ModTime().Equal(fi2.ModTime()) &&
2725
fi1.IsDir() == fi2.IsDir() &&
2826
os.SameFile(fi1, fi2)
2927
}
@@ -38,10 +36,6 @@ func testUnixDirentParallel(t *testing.T, ent *unixDirent, want fs.FileInfo,
3836

3937
var wg sync.WaitGroup
4038
start := make(chan struct{})
41-
var mu sync.Mutex
42-
infos := make(map[*fileInfo]int)
43-
stats := make(map[*fileInfo]int)
44-
4539
for i := 0; i < numCPU; i++ {
4640
wg.Add(1)
4741
go func() {
@@ -53,12 +47,6 @@ func testUnixDirentParallel(t *testing.T, ent *unixDirent, want fs.FileInfo,
5347
t.Error(err)
5448
return
5549
}
56-
info := (*fileInfo)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&ent.info))))
57-
stat := (*fileInfo)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&ent.stat))))
58-
mu.Lock()
59-
infos[info]++
60-
stats[stat]++
61-
mu.Unlock()
6250
if !sameFile(fi, want) {
6351
t.Errorf("FileInfo not equal:\nwant: %s\ngot: %s\n",
6452
FormatFileInfo(want), FormatFileInfo(fi))
@@ -70,8 +58,6 @@ func testUnixDirentParallel(t *testing.T, ent *unixDirent, want fs.FileInfo,
7058

7159
close(start)
7260
wg.Wait()
73-
74-
t.Logf("Infos: %d Stats: %d\n", len(infos), len(stats))
7561
}
7662

7763
func TestUnixDirent(t *testing.T) {
@@ -222,24 +208,6 @@ func TestSortDirents(t *testing.T) {
222208
})
223209
}
224210

225-
func BenchmarkUnixDirentLoadFileInfo(b *testing.B) {
226-
wd, err := os.Getwd()
227-
if err != nil {
228-
b.Fatal(err)
229-
}
230-
fi, err := os.Lstat(wd)
231-
if err != nil {
232-
b.Fatal(err)
233-
}
234-
parent, name := filepath.Split(wd)
235-
d := newUnixDirent(parent, name, fi.Mode().Type(), 0)
236-
237-
for i := 0; i < b.N; i++ {
238-
loadFileInfo(&d.info)
239-
d.info = nil
240-
}
241-
}
242-
243211
func BenchmarkUnixDirentInfo(b *testing.B) {
244212
wd, err := os.Getwd()
245213
if err != nil {
@@ -250,38 +218,26 @@ func BenchmarkUnixDirentInfo(b *testing.B) {
250218
b.Fatal(err)
251219
}
252220
parent, name := filepath.Split(wd)
253-
d := newUnixDirent(parent, name, fi.Mode().Type(), 0)
254221

255-
for i := 0; i < b.N; i++ {
256-
fi, err := d.Info()
257-
if err != nil {
258-
b.Fatal(err)
259-
}
260-
if fi == nil {
261-
b.Fatal("Nil FileInfo")
222+
b.Run("Serial", func(b *testing.B) {
223+
d := newUnixDirent(parent, name, fi.Mode().Type(), 0)
224+
for i := 0; i < b.N; i++ {
225+
_, err := d.Info()
226+
if err != nil {
227+
b.Fatal(err)
228+
}
262229
}
263-
}
264-
}
265-
266-
func BenchmarkUnixDirentStat(b *testing.B) {
267-
wd, err := os.Getwd()
268-
if err != nil {
269-
b.Fatal(err)
270-
}
271-
fi, err := os.Lstat(wd)
272-
if err != nil {
273-
b.Fatal(err)
274-
}
275-
parent, name := filepath.Split(wd)
276-
d := newUnixDirent(parent, name, fi.Mode().Type(), 0)
230+
})
277231

278-
for i := 0; i < b.N; i++ {
279-
fi, err := d.Stat()
280-
if err != nil {
281-
b.Fatal(err)
282-
}
283-
if fi == nil {
284-
b.Fatal("Nil FileInfo")
285-
}
286-
}
232+
b.Run("Parallel", func(b *testing.B) {
233+
b.RunParallel(func(pb *testing.PB) {
234+
d := newUnixDirent(parent, name, fi.Mode().Type(), 0)
235+
for pb.Next() {
236+
_, err := d.Info()
237+
if err != nil {
238+
b.Fatal(err)
239+
}
240+
}
241+
})
242+
})
287243
}

0 commit comments

Comments
 (0)