From bb0c14b895d90bb5941e0463ba6c3564fc504e4f Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 13 May 2025 17:26:06 +0200 Subject: [PATCH] os: don't fallback to the Stat slow path if file doesn't exist on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit os.Stat and os.Lstat first try stating the file without opening it. If that fails, then they open the file and try again, operations that tends to be slow. There is no point in trying the slow path if the file doesn't exist, we should just return an error immediately. This CL makes stating a non-existent file on Windows 50% faster: goos: windows goarch: amd64 pkg: os cpu: Intel(R) Core(TM) i7-10850H CPU @ 2.70GHz │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ StatNotExist-12 43.65µ ± 15% 20.02µ ± 10% -54.14% (p=0.000 n=10+7) │ old.txt │ new.txt │ │ B/op │ B/op vs base │ StatNotExist-12 224.0 ± 0% 224.0 ± 0% ~ (p=1.000 n=10+7) ¹ ¹ all samples are equal │ old.txt │ new.txt │ │ allocs/op │ allocs/op vs base │ StatNotExist-12 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=10+7) ¹ Updates #72992. Change-Id: Iaeb9596d0d18e5a5a1bd1970e296a3480501af78 Reviewed-on: https://go-review.googlesource.com/c/go/+/671458 LUCI-TryBot-Result: Go LUCI Reviewed-by: Jake Bailey Reviewed-by: Damien Neil Reviewed-by: Michael Knyszek --- src/os/stat_test.go | 15 +++++++++++++++ src/os/stat_windows.go | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/src/os/stat_test.go b/src/os/stat_test.go index 36da573f0c..92ecabb787 100644 --- a/src/os/stat_test.go +++ b/src/os/stat_test.go @@ -361,3 +361,18 @@ func TestClosedStat(t *testing.T) { t.Errorf("error from Stat on closed file did not match ErrClosed: %q, type %T", err, err) } } + +func TestStatNotExist(t *testing.T) { + t.Parallel() + name := filepath.Join(t.TempDir(), "notfound") + _, err := os.Stat(name) + if !errors.Is(err, fs.ErrNotExist) { + t.Errorf("os.Stat(%q) = %v; want fs.ErrNotExist", name, err) + } + + name = filepath.Join(t.TempDir(), "notfounddir", "notfound") + _, err = os.Stat(name) + if !errors.Is(err, fs.ErrNotExist) { + t.Errorf("os.Stat(%q) = %v; want fs.ErrNotExist", name, err) + } +} diff --git a/src/os/stat_windows.go b/src/os/stat_windows.go index d2c2017a65..e2ed58a3a0 100644 --- a/src/os/stat_windows.go +++ b/src/os/stat_windows.go @@ -5,6 +5,7 @@ package os import ( + "errors" "internal/filepathlite" "internal/syscall/windows" "syscall" @@ -34,6 +35,9 @@ func stat(funcname, name string, followSurrogates bool) (FileInfo, error) { // See https://golang.org/issues/19922#issuecomment-300031421 for details. var fa syscall.Win32FileAttributeData err = syscall.GetFileAttributesEx(namep, syscall.GetFileExInfoStandard, (*byte)(unsafe.Pointer(&fa))) + if errors.Is(err, ErrNotExist) { + return nil, &PathError{Op: "GetFileAttributesEx", Path: name, Err: err} + } if err == nil && fa.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 { // Not a surrogate for another named entity, because it isn't any kind of reparse point. // The information we got from GetFileAttributesEx is good enough for now. -- 2.51.0