internal/lsp: change file system to allow lazy reads

We split aquiring a "handle" from reading a files contents so that we can do the
former eagerly and the latter lazily.
We also "version" the handles so that the same file at different versions is a
different handle.

Change-Id: I06cc346d4b4c77d784aa454702c54689f2f177e0
Reviewed-on: https://go-review.googlesource.com/c/tools/+/179917
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Ian Cottrell 2019-05-31 19:41:39 -04:00
parent 8aaa1484dc
commit 2c3de6a5ae
6 changed files with 106 additions and 33 deletions

View File

@ -20,6 +20,7 @@ import (
func New() source.Cache { func New() source.Cache {
index := atomic.AddInt64(&cacheIndex, 1) index := atomic.AddInt64(&cacheIndex, 1)
c := &cache{ c := &cache{
fs: &nativeFileSystem{},
id: strconv.FormatInt(index, 10), id: strconv.FormatInt(index, 10),
fset: token.NewFileSet(), fset: token.NewFileSet(),
} }
@ -28,19 +29,22 @@ func New() source.Cache {
} }
type cache struct { type cache struct {
nativeFileSystem fs source.FileSystem
id string id string
fset *token.FileSet fset *token.FileSet
} }
func (c *cache) GetFile(uri span.URI) source.FileHandle {
return c.fs.GetFile(uri)
}
func (c *cache) NewSession(log xlog.Logger) source.Session { func (c *cache) NewSession(log xlog.Logger) source.Session {
index := atomic.AddInt64(&sessionIndex, 1) index := atomic.AddInt64(&sessionIndex, 1)
s := &session{ s := &session{
cache: c, cache: c,
id: strconv.FormatInt(index, 10), id: strconv.FormatInt(index, 10),
log: log, log: log,
overlays: make(map[span.URI]*source.FileContent), overlays: make(map[span.URI]*overlay),
filesWatchMap: NewWatchMap(), filesWatchMap: NewWatchMap(),
} }
debug.AddSession(debugSession{s}) debug.AddSession(debugSession{s})

View File

@ -5,6 +5,7 @@
package cache package cache
import ( import (
"context"
"io/ioutil" "io/ioutil"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
@ -14,9 +15,35 @@ import (
// nativeFileSystem implements FileSystem reading from the normal os file system. // nativeFileSystem implements FileSystem reading from the normal os file system.
type nativeFileSystem struct{} type nativeFileSystem struct{}
func (nativeFileSystem) ReadFile(uri span.URI) *source.FileContent { // nativeFileHandle implements FileHandle for nativeFileSystem
r := &source.FileContent{URI: uri} type nativeFileHandle struct {
filename, err := uri.Filename() fs *nativeFileSystem
identity source.FileIdentity
}
func (fs *nativeFileSystem) GetFile(uri span.URI) source.FileHandle {
return &nativeFileHandle{
fs: fs,
identity: source.FileIdentity{
URI: uri,
// TODO: decide what the version string is for a native file system
// could be the mtime?
Version: "",
},
}
}
func (h *nativeFileHandle) FileSystem() source.FileSystem {
return h.fs
}
func (h *nativeFileHandle) Identity() source.FileIdentity {
return h.identity
}
func (h *nativeFileHandle) Read(ctx context.Context) *source.FileContent {
r := &source.FileContent{}
filename, err := h.identity.URI.Filename()
if err != nil { if err != nil {
r.Error = err r.Error = err
return r return r

View File

@ -85,5 +85,5 @@ func (f *fileBase) read(ctx context.Context) {
} }
} }
// We don't know the content yet, so read it. // We don't know the content yet, so read it.
f.fc = f.view.Session().ReadFile(f.URI()) f.fc = f.view.Session().GetFile(f.URI()).Read(ctx)
} }

View File

@ -31,12 +31,18 @@ type session struct {
viewMap map[span.URI]source.View viewMap map[span.URI]source.View
overlayMu sync.Mutex overlayMu sync.Mutex
overlays map[span.URI]*source.FileContent overlays map[span.URI]*overlay
openFiles sync.Map openFiles sync.Map
filesWatchMap *WatchMap filesWatchMap *WatchMap
} }
type overlay struct {
session *session
uri span.URI
content source.FileContent
}
func (s *session) Shutdown(ctx context.Context) { func (s *session) Shutdown(ctx context.Context) {
s.viewMu.Lock() s.viewMu.Lock()
defer s.viewMu.Unlock() defer s.viewMu.Unlock()
@ -186,12 +192,12 @@ func (s *session) IsOpen(uri span.URI) bool {
return open return open
} }
func (s *session) ReadFile(uri span.URI) *source.FileContent { func (s *session) GetFile(uri span.URI) source.FileHandle {
if overlay := s.readOverlay(uri); overlay != nil { if overlay := s.readOverlay(uri); overlay != nil {
return overlay return overlay
} }
// Fall back to the cache-level file system. // Fall back to the cache-level file system.
return s.Cache().ReadFile(uri) return s.Cache().GetFile(uri)
} }
func (s *session) SetOverlay(uri span.URI, data []byte) { func (s *session) SetOverlay(uri span.URI, data []byte) {
@ -205,14 +211,17 @@ func (s *session) SetOverlay(uri span.URI, data []byte) {
delete(s.overlays, uri) delete(s.overlays, uri)
return return
} }
s.overlays[uri] = &source.FileContent{ s.overlays[uri] = &overlay{
URI: uri, session: s,
Data: data, uri: uri,
Hash: hashContents(data), content: source.FileContent{
Data: data,
Hash: hashContents(data),
},
} }
} }
func (s *session) readOverlay(uri span.URI) *source.FileContent { func (s *session) readOverlay(uri span.URI) *overlay {
s.overlayMu.Lock() s.overlayMu.Lock()
defer s.overlayMu.Unlock() defer s.overlayMu.Unlock()
@ -229,16 +238,31 @@ func (s *session) buildOverlay() map[string][]byte {
overlays := make(map[string][]byte) overlays := make(map[string][]byte)
for uri, overlay := range s.overlays { for uri, overlay := range s.overlays {
if overlay.Error != nil { if overlay.content.Error != nil {
continue continue
} }
if filename, err := uri.Filename(); err == nil { if filename, err := uri.Filename(); err == nil {
overlays[filename] = overlay.Data overlays[filename] = overlay.content.Data
} }
} }
return overlays return overlays
} }
func (o *overlay) FileSystem() source.FileSystem {
return o.session
}
func (o *overlay) Identity() source.FileIdentity {
return source.FileIdentity{
URI: o.uri,
Version: o.content.Hash,
}
}
func (o *overlay) Read(ctx context.Context) *source.FileContent {
return &o.content
}
type debugSession struct{ *session } type debugSession struct{ *session }
func (s debugSession) ID() string { return s.id } func (s debugSession) ID() string { return s.id }
@ -258,15 +282,15 @@ func (s debugSession) Files() []*debug.File {
s.overlayMu.Lock() s.overlayMu.Lock()
defer s.overlayMu.Unlock() defer s.overlayMu.Unlock()
for _, overlay := range s.overlays { for _, overlay := range s.overlays {
f, ok := seen[overlay.URI] f, ok := seen[overlay.uri]
if !ok { if !ok {
f = &debug.File{Session: s, URI: overlay.URI} f = &debug.File{Session: s, URI: overlay.uri}
seen[overlay.URI] = f seen[overlay.uri] = f
files = append(files, f) files = append(files, f)
} }
f.Data = string(overlay.Data) f.Data = string(overlay.content.Data)
f.Error = overlay.Error f.Error = overlay.content.Error
f.Hash = overlay.Hash f.Hash = overlay.content.Hash
} }
sort.Slice(files, func(i int, j int) bool { sort.Slice(files, func(i int, j int) bool {
return files[i].URI < files[j].URI return files[i].URI < files[j].URI
@ -278,13 +302,13 @@ func (s debugSession) File(hash string) *debug.File {
s.overlayMu.Lock() s.overlayMu.Lock()
defer s.overlayMu.Unlock() defer s.overlayMu.Unlock()
for _, overlay := range s.overlays { for _, overlay := range s.overlays {
if overlay.Hash == hash { if overlay.content.Hash == hash {
return &debug.File{ return &debug.File{
Session: s, Session: s,
URI: overlay.URI, URI: overlay.uri,
Data: string(overlay.Data), Data: string(overlay.content.Data),
Error: overlay.Error, Error: overlay.content.Error,
Hash: overlay.Hash, Hash: overlay.content.Hash,
} }
} }
} }

View File

@ -18,19 +18,37 @@ import (
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
) )
// FileContents is returned from FileSystem implementation to represent the // FileContent is returned from FileSystem implementation to represent the
// contents of a file. // contents of a file.
type FileContent struct { type FileContent struct {
URI span.URI
Data []byte Data []byte
Error error Error error
Hash string Hash string
} }
// FileIdentity uniquely identifies a file at a version from a FileSystem.
type FileIdentity struct {
URI span.URI
Version string
}
// FileHandle represents a handle to a specific version of a single file from
// a specific file system.
type FileHandle interface {
// FileSystem returns the file system this handle was aquired from.
FileSystem() FileSystem
// Return the Identity for the file.
Identity() FileIdentity
// Read reads the contents of a file and returns it.
// If the file is not available, the returned FileContent will have no
// data and an error.
Read(ctx context.Context) *FileContent
}
// FileSystem is the interface to something that provides file contents. // FileSystem is the interface to something that provides file contents.
type FileSystem interface { type FileSystem interface {
// ReadFile reads the contents of a file and returns it. // GetFile returns a handle for the specified file.
ReadFile(uri span.URI) *FileContent GetFile(uri span.URI) FileHandle
} }
// Cache abstracts the core logic of dealing with the environment from the // Cache abstracts the core logic of dealing with the environment from the

View File

@ -66,7 +66,7 @@ func (s *Server) applyChanges(ctx context.Context, params *protocol.DidChangeTex
} }
uri := span.NewURI(params.TextDocument.URI) uri := span.NewURI(params.TextDocument.URI)
fc := s.session.ReadFile(uri) fc := s.session.GetFile(uri).Read(ctx)
if fc.Error != nil { if fc.Error != nil {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found") return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
} }