-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
proposal: os: make Readdir return lazy FileInfo implementations #41188
Comments
Last time this was proposed there was debate about what the behavior for |
I think this is likely to introduce subtle changes in behavior. Perhaps more importantly, I don't think this is the sort of change that we can reliably verify during a development cycle. In my experience, very few users who are not either Go contributors or Googlers test Beta or RC releases of the Go toolchain, and changes in the |
os.FileInfo can't be lazy as-is, because it can't return an error. Returning a 0 size on transient errors is not acceptable. |
See also #40352, which is about different approaches to efficiently uncover similar information. |
(Ignoring the POSIX API for a moment) NFS, and likely many other network filesystems, can do completely separate operations depending on whether the stat info is going to be needed (NFSv3 readdir vs readdirplus, NFSv4 "bulk LOOKUP", FUSE_READDIRPLUS). There's also been a lot of talk about a Linux syscall that would fetch getdents+lstat info, for example https://lwn.net/Articles/606995/ -- they all seem to revolve around the idea of the client knowing beforehand whether it will be doing the lstat calls or not, and communicating that to the kernel. These combined make me think the right path forward would be a Readdir method that takes arguments that inform it which os.FileInfo fields will be wanted; the rest could be zero values. (That extended Readdir could also take a flag for whether to sort the results or not, removing one common cause of forks for performance reasons.) |
EDIT: This has a detailed proposal in #41265 I believe we need a new dirent abstraction. After reviewing suggestions from the FS API discussion...
Let's consider a hybrid:
That solves the EDIT: The interface which
Rationale: |
Wasn't the Windows FileId 128-bit? (Seen somewhere on go issues around the greater topic in the last few days.) Either way, the unix inode number isn't very useful without the device major:minor. For example, you can't Also, inode number and such belong in a |
I gave the .Id() type as Today on Windows, FileInfo.Sys() can't even provide the fileId! Adding it was debated and discarded. Is there any practical value in .Sys() besides providing .Ino on unix? Winapi fileId is 64-bit on NTFS, 128-bit on ReFS (an alternative for Windows Server): |
I really like the intent, but I agree with other commenters that the API just doesn't quite fit as is, because of potential errors returned by the lazy methods. We actually debated a very similar issue when designing the os.scandir / os.DirEntry API in Python. At first we wanted to make the DirEntry methods properties, like
I believe this decision was based on theoretical concerns, not from actual testing, but still, the logic seems sound. Especially in Go, where all error handling is super-explicit (we always want "find grained control over errors"). Panic-ing is not going to work, and silently returning a zero value is arguably worse. |
I have always been under the impression that calling a method on This leads me to believe that saving those information should be up to the user, the caller, to decide when and what to store. The Another solution would probably be to dirtily make a new // Reset the os.FileInfo to fetch new information.
if resetter, ok := fileInfo.(os.InfoResetter); ok {
resetter.ResetInfo()
} Although this solution still has the behavior that a |
There's nothing theoretical about
Can't, it has no way to communicate errors. Repeating earlier suggestion, more concretely:
If this is going to be used for pluggable filesystems (#41190), then it would probably be nicer to find a way for each filesystem to be able to control FS-specific extensions to FileInfo, instead of using a bitmap with stdlib-decided values. That would serve |
@tv42 This API seems too convoluted comparing to the current API. It also introduces an inconsistency between |
@diamondburned fixing the spelling of Readdir is part of https://go.googlesource.com/proposal/+/master/design/draft-iofs.md |
@networkimprov |
I'll make a second attempt. What if we make // DirEnt describes a directory entity.
type DirEnt interface {
Name() string
IsDir() bool
Lstat() (FileInfo, error)
}
var _ DirEnt = (*dirEnt)(nil)
func (f *File) ReadDir(n int) ([]DirEnt, error)
// Windows impl:
func (e *dirEnt) Lstat() (FileInfo, error) {
return e.finfo, nil
}
// Unix impl:
func (e *dirEnt) Lstat() (FileInfo, error) {
return lstat(e.name)
}
// Custom impl where getting a FileInfo wouldn't error:
func (e *dirEnt) Lstat() (FileInfo, error) {
return e, nil
} This new |
@tv42 your fields argument is in my I've added a Rationale section to #41188 (comment) describing cases for the I drafted that API after a careful reading of all discussion on this issue, and believe it maximizes performance and usability. |
@networkimprov Your API suggestion chases readdir with reading the stats, my API suggestion gives readdir the information needed to do the right thing. I believe a FS should have the fields up front, to be able to fetch them during the directory reading, and doing that afterward will never be the same. I'm not sure I can use more words to describe that again, and you seem to have completely skipped over the difference. |
You misunderstood. #41188 (comment) allows both Each approach performs better in certain cases, listed in Rationale. (Either way, all OS-default fields are provided, even if not requested, as it's no extra overhead.) |
@networkimprov Oh I missed the |
I filed #41265. It offers a new ReadDir() API for io/fs. |
I intend to do a survey of actual uses in the Go corpus, but I haven't finished it yet. |
I grepped through my Go corpus for Readdir/ReadDir calls. That turned up about 55,000 calls. See readdirs.txt, attached. I randomly sampled 200 calls from that set; 6 were false positives (comments, functions named ReadDir but not the one we are talking about, and so on), and I randomly sampled 6 more to get back to 200 real samples. I classified all 200 samples, looking at how the result of ReadDir was used. Of the 200 uses of ReadDir, the breakdown is:
Of the 200, none of them saved the FileInfo slice for some future use. They all looked at the results immediately, meaning they would not have a chance to observe file system modifications sequenced between the ReadDir and the method calls. Also, 35+90+52 = 177/200 = 88% of the calls would end up with no delayed (lazy) Stat calls at all - they never call any methods other than Name and IsDir. Those would be made much faster (and also guaranteed never to see any kind of race or inconsistency, since they never call any of the “extra work” methods). This gives an estimate, to within maybe 0.5%, that about 88% of programs would get faster with no possibility of noticing the laziness. And about 12% would run about the same speed, invoking the lazy extra work. These would not see lazy inconsistencies caused by any of their own actions, but they might notice zeroed modification times or sizes if files are being deleted out from under them by other processes (likely rare). Again, best estimate, to within about 0.5%:
If you want to see my work, see the #-prefixed notes in readdir200.txt, also attached. Overall, it seems like a low-risk large win. I'm particularly happy about the number of programs that just start working much faster with no rewriting required. |
I'd strongly prefer -1 rather than 0. We could also define a special modebit for "stale FileInfo". I'll be on record to say this is a bad idea as well. It's not clear why we are designing a new FS abstraction and inheriting what is clearly a mistake of the past. The only reason I see is that this is rushed to have embed.Files in 1.16, and this lazy FileInfo is probably the only solution not to miss the 1.16 deadline. I'm not sure it's a good idea. |
I'd still say that the better solution would be the first option that @mpx has listed, that is to provide a new API that returns a list of Dirents. Existing code will not get that free performance boost regarding |
I'm not sure how I feel about this idea overall, but we could indeed add an error return to |
@ianlancetaylor, I'm not sure that a |
#40352 (an impetus for this issue) requested a way to see fields from the native dirent struct. It proposed to optionally lazy-load the data for For io/fs, retrieving all metadata in For regular storage, So we need a faster, richer os API, which I explore in #41265. But that's not a prerequisite for io/fs. |
@bcmills I'll argue FileInfo.Size returning -1 on rainy days is a violation of Go1 compatibility promise; -1 is not a "length in bytes", for a regular file seen by ReadDir. Same for Mode having an "error bit". Also, what would IsDir do, it returns a bool. I'm, again, surprised these ideas are considered worth considering! Just add a way for me to tell the system, up front, that I don't care about the FileInfo data; call that ReadDirEntries or an argument for ReadDir or whatever, let the caller explicitly state intent. That removes all this ambiguity and "rainy day syndrome". |
Russ has posted to say (IIUC) that a new API to solve this problem is off the table: #41265 (comment) Disappointing. I guess enough folks haven't publicly flagged the problem to date, alas. |
For what it's worth, I believe that SameFile might actually work fine with the lazy API, since one thing you do get from the directory read on Unix systems is the inode number. That said, it doesn't invalidate the general hypothetical, but I still have yet to see a concrete case that would break. |
That's not the right metric for sampling: the total number of samples is what matters. Consider these two cases:
In both cases, you only looked at 0.1% of the balls, but the conclusions you can draw are very different, because what matters is the number of samples, not the overall fraction of the total you were drawing from. All this is assuming random sampling. If you are taking balls from the top of the bucket but all the red ones are on the bottom, none of this applies. That's why I had a program pick random samples for me. |
Did the analysis consider cases that simply return the results of For example, MacOS directories may include ".DS_Store" files created by the OS. In apps that support MacOS, I wrap Thanks for the stats tutorial :-) |
Yes, in all the sampled case I read enough of the code to find out what was happening with the results. That included looking at callers as needed. |
For the record, I understand that everyone's intuition, including mine, is that this seems like a problematic change. |
My major concern is old programs getting the new sentinel values used to mark errors and not processing them correctly. I have no issue with a new api that makes these things explicit. |
This continues to mystify me.
Surely we all know there's no such thing as "immediately", and that the filesystem may change between ReadDir doing Lines 360 to 364 in 6b42016
In Russ's design, that race window is significantly larger, and simply cannot be handled right. Why are we discussing this?! I apologize up front if the following comes across as too personal, but I feel like I have to say this. |
I think this proposal trades correctness & simplicity for performance, as well as breaking the Go1 compatiblity promise. In the past I've really appreciated that Go hasn't made this tradeoff and has found other ways of improving performance. This kind of behaviour is better suited to All existing programs have (implicitly or explicitly) been written with the assumption that
Is this assuming that In future, programs would need to check the In practice, many developers will not check the results from Using it correctly would be extra hassle: sz := fi.Size()
if sz < 0 {
// Some unknown error occurred, retry the operation to obtain the error or obtain a valid FileInfo.
fi, err = os.Lstat(filepath.Join(f.Name(), fi.Name()))
if err != nil {
// Error handling
}
}
// Use size. APIs that guarantee correctness without needing explicit error handling are extremely useful (eg, I want to use If adding a method to
This might be less controversial than deferring stat? |
@mpx I like what you said, but: Dirent.IsDir is impossible. Linux dirent d_type generally communicates more than just IsDir (DT_LNK etc), but the direntry type can be unknown. There's no way to write an |
For the sake of completeness, couldn't there be a fallback to func (d *dirEnt) IsDir() bool {
if d.stat != nil {
return d.stat.isDir
}
return d.isDir
} Although this no longer completely satisfies the goals of this issue (i.e. having a I can see another problem with this API though: |
While I am generally in favor, discussing the specifics of a new api is premature when it hasn't been decided if it's necessary yet. |
I certainly hear you all about the change being strange. I agree it's a bit odd. The reason I'm trying hard to find a path forward here is that I'm trying to balance a few different concerns:
Allowing lazy Readdir elegantly solves almost all of this, at the cost of the lazy behavior that seems from direct code inspection not to matter as much as you'd initially think. If we don't fix this problem now, we will be stuck with programs like goimports having their own custom filepath.Walk, and worse there will be no way to write a custom filepath.Walk for the general FS implementations. If there's not consensus on the lazy Readdir - as there does not seem to be - then it still seems worth trying to fix the problem another way. Whatever we do, it needs to be a limited change: a simple, Go-like API. I expanded @mpx's suggestion above into a separate proposal, #41467. Please see the description and comment over there. Thanks. |
Retracting per discussion above; see #41467. |
An
os.File
provides two ways to read a directory:Readdirnames
returns a list of the names of the directory entries, andReaddir
returns the names along with stat information.On Plan 9 and Windows,
Readdir
can be implemented with only a directory read - the directory read operation provides the full stat information.But many Go users use Unix systems.
On most Unix systems, the directory read does not provide full stat information. So the implementation of Readdir reads the names from the directory and then calls Lstat for each file. This is fairly expensive.
Much of the time, such as in the implementation of filepath.Glob and other file system walking, the only information the caller of
Readdir
really needs is the name and whether the name denotes a directory. On most Unix systems, that single bit of information—is this name a directory?—is available from the plain directory read, without an additional stat. If the caller is only using that bit, the extra Lstat calls are unnecessary and slow. (Goimports, for example, has its own directory walker to avoid this cost.)Various people have proposed adding a third directory reading option of one form or another, to get names and IsDir bits. This would certainly address the slow directory walk issue on Unix systems, but it seems like overfitting to Unix.
Note that
os.FileInfo
is an interface. What if we makeReaddir
return a slice of lazily-filledos.FileInfo
? That is, on Unix,Readdir
would stop callingLstat
. Each returnedFileInfo
would already know the answer for itsName
andIsDir
methods. The first call to any of the other methods would incur anLstat
at that moment to find out the rest of the information. A directory walk that usesReaddir
and then only callsName
andIsDir
would have all itsLstat
calls optimized away with no code changes in the caller.The downside of this is that the laziness would be visible when you do the
Readdir
and wait a while before looking at the results. For example if you didReaddir
, then touched one of the files in the list, then called theModTime
method on theos.FileInfo
thatReaddir
retruned, you'd see the updated modification time. And then if you touched the file again and calledModTime
again, you wouldn't see the further-updated modification time. That could be confusing. But I expect that the vast majority of uses ofReaddir
use the results immediately or at least before making changes to files listed in the results. I suspect the vast majority of users would not notice this change.I propose we make this change—make
Readdir
return lazyos.FileInfo
—soon, intending it to land in Go 1.16, but ready to roll back the change if the remainder of the Go 1.16 dev cycle or beta/rc testing turns up important problems with it./cc @robpike @bradfitz @ianthehat @kr
The text was updated successfully, but these errors were encountered: