diff options
Diffstat (limited to 'gopls/internal/lsp/cache/fs_memoized.go')
-rw-r--r-- | gopls/internal/lsp/cache/fs_memoized.go | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/fs_memoized.go new file mode 100644 index 000000000..9acd87276 --- /dev/null +++ b/gopls/internal/lsp/cache/fs_memoized.go @@ -0,0 +1,149 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "context" + "os" + "sync" + "time" + + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/robustio" +) + +// A memoizedFS is a file source that memoizes reads, to reduce IO. +type memoizedFS struct { + mu sync.Mutex + + // filesByID maps existing file inodes to the result of a read. + // (The read may have failed, e.g. due to EACCES or a delete between stat+read.) + // Each slice is a non-empty list of aliases: different URIs. + filesByID map[robustio.FileID][]*DiskFile +} + +func newMemoizedFS() *memoizedFS { + return &memoizedFS{filesByID: make(map[robustio.FileID][]*DiskFile)} +} + +// A DiskFile is a file on the filesystem, or a failure to read one. +// It implements the source.FileHandle interface. +type DiskFile struct { + uri span.URI + modTime time.Time + content []byte + hash source.Hash + err error +} + +func (h *DiskFile) URI() span.URI { return h.uri } + +func (h *DiskFile) FileIdentity() source.FileIdentity { + return source.FileIdentity{ + URI: h.uri, + Hash: h.hash, + } +} + +func (h *DiskFile) Saved() bool { return true } +func (h *DiskFile) Version() int32 { return 0 } +func (h *DiskFile) Read() ([]byte, error) { return h.content, h.err } + +// GetFile stats and (maybe) reads the file, updates the cache, and returns it. +func (fs *memoizedFS) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { + id, mtime, err := robustio.GetFileID(uri.Filename()) + if err != nil { + // file does not exist + return &DiskFile{ + err: err, + uri: uri, + }, nil + } + + // We check if the file has changed by comparing modification times. Notably, + // this is an imperfect heuristic as various systems have low resolution + // mtimes (as much as 1s on WSL or s390x builders), so we only cache + // filehandles if mtime is old enough to be reliable, meaning that we don't + // expect a subsequent write to have the same mtime. + // + // The coarsest mtime precision we've seen in practice is 1s, so consider + // mtime to be unreliable if it is less than 2s old. Capture this before + // doing anything else. + recentlyModified := time.Since(mtime) < 2*time.Second + + fs.mu.Lock() + fhs, ok := fs.filesByID[id] + if ok && fhs[0].modTime.Equal(mtime) { + var fh *DiskFile + // We have already seen this file and it has not changed. + for _, h := range fhs { + if h.uri == uri { + fh = h + break + } + } + // No file handle for this exact URI. Create an alias, but share content. + if fh == nil { + newFH := *fhs[0] + newFH.uri = uri + fh = &newFH + fhs = append(fhs, fh) + fs.filesByID[id] = fhs + } + fs.mu.Unlock() + return fh, nil + } + fs.mu.Unlock() + + // Unknown file, or file has changed. Read (or re-read) it. + fh, err := readFile(ctx, uri, mtime) // ~25us + if err != nil { + return nil, err // e.g. cancelled (not: read failed) + } + + fs.mu.Lock() + if !recentlyModified { + fs.filesByID[id] = []*DiskFile{fh} + } else { + delete(fs.filesByID, id) + } + fs.mu.Unlock() + return fh, nil +} + +// ioLimit limits the number of parallel file reads per process. +var ioLimit = make(chan struct{}, 128) + +func readFile(ctx context.Context, uri span.URI, mtime time.Time) (*DiskFile, error) { + select { + case ioLimit <- struct{}{}: + case <-ctx.Done(): + return nil, ctx.Err() + } + defer func() { <-ioLimit }() + + ctx, done := event.Start(ctx, "cache.readFile", tag.File.Of(uri.Filename())) + _ = ctx + defer done() + + // It is possible that a race causes us to read a file with different file + // ID, or whose mtime differs from the given mtime. However, in these cases + // we expect the client to notify of a subsequent file change, and the file + // content should be eventually consistent. + content, err := os.ReadFile(uri.Filename()) // ~20us + if err != nil { + content = nil // just in case + } + return &DiskFile{ + modTime: mtime, + uri: uri, + content: content, + hash: source.HashOf(content), + err: err, + }, nil +} |