internal/lsp: using memoize for all file contents

Change-Id: I7293bbcfea33c0df90f7b6df8e991aae9a03f2ab
Reviewed-on: https://go-review.googlesource.com/c/tools/+/180846
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index 81a8c77..b14fc9d 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -5,6 +5,7 @@
 package cache
 
 import (
+	"context"
 	"crypto/sha1"
 	"fmt"
 	"go/token"
@@ -14,6 +15,7 @@
 	"golang.org/x/tools/internal/lsp/debug"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/xlog"
+	"golang.org/x/tools/internal/memoize"
 	"golang.org/x/tools/internal/span"
 )
 
@@ -32,10 +34,42 @@
 	fs   source.FileSystem
 	id   string
 	fset *token.FileSet
+
+	store memoize.Store
+}
+
+type fileKey struct {
+	identity source.FileIdentity
+}
+
+type fileHandle struct {
+	cache      *cache
+	underlying source.FileHandle
+	handle     *memoize.Handle
+}
+
+type fileData struct {
+	memoize.NoCopy
+	bytes []byte
+	hash  string
+	err   error
 }
 
 func (c *cache) GetFile(uri span.URI) source.FileHandle {
-	return c.fs.GetFile(uri)
+	underlying := c.fs.GetFile(uri)
+	key := fileKey{
+		identity: underlying.Identity(),
+	}
+	h := c.store.Bind(key, func(ctx context.Context) interface{} {
+		data := &fileData{}
+		data.bytes, data.hash, data.err = underlying.Read(ctx)
+		return data
+	})
+	return &fileHandle{
+		cache:      c,
+		underlying: underlying,
+		handle:     h,
+	}
 }
 
 func (c *cache) NewSession(log xlog.Logger) source.Session {
@@ -55,6 +89,23 @@
 	return c.fset
 }
 
+func (h *fileHandle) FileSystem() source.FileSystem {
+	return h.cache
+}
+
+func (h *fileHandle) Identity() source.FileIdentity {
+	return h.underlying.Identity()
+}
+
+func (h *fileHandle) Read(ctx context.Context) ([]byte, string, error) {
+	v := h.handle.Get(ctx)
+	if v == nil {
+		return nil, "", ctx.Err()
+	}
+	data := v.(*fileData)
+	return data.bytes, data.hash, data.err
+}
+
 func hashContents(contents []byte) string {
 	// TODO: consider whether sha1 is the best choice here
 	// This hash is used for internal identity detection only
diff --git a/internal/lsp/cache/external.go b/internal/lsp/cache/external.go
index d2b8286..65f1cb5 100644
--- a/internal/lsp/cache/external.go
+++ b/internal/lsp/cache/external.go
@@ -42,6 +42,7 @@
 }
 
 func (h *nativeFileHandle) Read(ctx context.Context) ([]byte, string, error) {
+	//TODO: this should fail if the version is not the same as the handle
 	data, err := ioutil.ReadFile(h.identity.URI.Filename())
 	if err != nil {
 		return nil, "", err
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 7d81fb0..40d3c1e 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -225,7 +225,11 @@
 // including any position and type information that depends on it.
 func (f *goFile) invalidateContent() {
 	f.view.pcache.mu.Lock()
-	defer f.view.pcache.mu.Unlock()
+	f.handleMu.Lock()
+	defer func() {
+		f.handleMu.Unlock()
+		f.view.pcache.mu.Unlock()
+	}()
 
 	f.ast = nil
 	f.token = nil