net/http.ServeContent or io.Copy to a net.TCPConn only uses the zero-copy sendfile(2) path when the source reader implements syscall.Conn. See net/sendfile.go. *os.File implements syscall.Conn, so serving a file straight from os.Open gets the sendfile(2) optimization. If you're not aware, sendfile(2) has a big impact on serving performance.
But when a file passes through a wrapping Fs (BasePathFs, ReadOnlyFs, RegexpFs, etc.) the wrapper file type does not expose syscall.Conn so net.sendFile falls back to a userspace read/write copy, losing the optimization. BasePathFs is a very common base, so in practice afero silently disables sendfile(2) for most static-file serving cases.
As a workaround I found that recursively type asserting afero.File into an afero.BasePathFile to access its underlying File field until I get an *os.File works ok in my case (see func osFile below), but this is awkward, undocumented, and not universal.
func osFile(f afero.File) (*os.File, bool) {
for {
switch v := f.(type) {
case *os.File: return v, true
case *afero.BasePathFile: f = v.File
default: return nil, false
}
}
}
Ideally users that use afero and don't know about sendfile(2) wouldn't have to pay a performance penalty when serving files over the network. Perhaps afero could return a different File type that does implement syscall.Conn. But then, whether the wrapper type implements syscall.Conn should depend on whether the wrapped type does, which in turn depends on exactly which file is opened because you could land on an afero layer that doesn't wrap an os filesystem at all like memmapfs which can't support sendfile/syscall.Conn. I don't think it would be too hard to do this just for the syscall.Conn interface, just recursively unwrap the file and if the bottom file type implements syscall.Conn then return an alternative afero type that also implements the interface in that case.
Note that to fully mirror the wrapped type you'd have to forward every optional interface it implements. For files that's a handful (syscall.Conn, io.ReaderFrom, io.WriterTo, …) but not nothing. It's the same problem httpsnoop.Wrap solves for http.ResponseWriter by generating 2^n implementations and picking the one that perfectly mirrors the source. Not asking for that here, syscall.Conn alone fixes my case.
A simpler but less smooth alternative is just explicitly documenting and canonicalizing the workaround above in a free function, say afero.UnwrapOsFile(afero.File) (*os.File, bool) or something more specific like afero.UnwrapSyscallConnFile(afero.File) (afero.File, bool) (where the returned afero.File may implement syscall.Conn if the underlying file does, forwarding calls to the unwrapped File type).
My preference would be BasePathFs.Open() etc returns a type that implements syscall.Conn if the backing fs does. Barring that, I'd be satisfied with a func that canonicalizes the workaround.
References:
net/http.ServeContentorio.Copyto anet.TCPConnonly uses the zero-copysendfile(2)path when the source reader implementssyscall.Conn. See net/sendfile.go.*os.Fileimplementssyscall.Conn, so serving a file straight fromos.Opengets thesendfile(2)optimization. If you're not aware,sendfile(2)has a big impact on serving performance.But when a file passes through a wrapping Fs (
BasePathFs,ReadOnlyFs,RegexpFs, etc.) the wrapper file type does not exposesyscall.Connsonet.sendFilefalls back to a userspace read/write copy, losing the optimization.BasePathFsis a very common base, so in practice afero silently disablessendfile(2)for most static-file serving cases.As a workaround I found that recursively type asserting
afero.Fileinto anafero.BasePathFileto access its underlyingFilefield until I get an*os.Fileworks ok in my case (seefunc osFilebelow), but this is awkward, undocumented, and not universal.Ideally users that use afero and don't know about sendfile(2) wouldn't have to pay a performance penalty when serving files over the network. Perhaps afero could return a different File type that does implement
syscall.Conn. But then, whether the wrapper type implementssyscall.Connshould depend on whether the wrapped type does, which in turn depends on exactly which file is opened because you could land on an afero layer that doesn't wrap an os filesystem at all like memmapfs which can't support sendfile/syscall.Conn. I don't think it would be too hard to do this just for thesyscall.Conninterface, just recursively unwrap the file and if the bottom file type implementssyscall.Connthen return an alternative afero type that also implements the interface in that case.Note that to fully mirror the wrapped type you'd have to forward every optional interface it implements. For files that's a handful (
syscall.Conn,io.ReaderFrom,io.WriterTo, …) but not nothing. It's the same problem httpsnoop.Wrap solves forhttp.ResponseWriterby generating 2^n implementations and picking the one that perfectly mirrors the source. Not asking for that here,syscall.Connalone fixes my case.A simpler but less smooth alternative is just explicitly documenting and canonicalizing the workaround above in a free function, say
afero.UnwrapOsFile(afero.File) (*os.File, bool)or something more specific likeafero.UnwrapSyscallConnFile(afero.File) (afero.File, bool)(where the returned afero.File may implementsyscall.Connif the underlying file does, forwarding calls to the unwrapped File type).My preference would be
BasePathFs.Open()etc returns a type that implementssyscall.Connif the backing fs does. Barring that, I'd be satisfied with a func that canonicalizes the workaround.References: