Skip to content

Wrapper files (BasePathFile, etc) hide *os.File's syscall.Conn, defeating net/http's sendfile(2) fast path #630

Description

@infogulch

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:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions