aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/cache/fs_memoized.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/cache/fs_memoized.go')
-rw-r--r--gopls/internal/lsp/cache/fs_memoized.go149
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
+}