gopls/internal/lsp/cache: actually preload files in parallel

snapshot.ReadFile holds the snapshot lock, so calling it concurrently
was pointless. Instead, add a new preloadFiles helper that delegates to
to the underlying FileSource.

For golang/go#57987

Change-Id: If3259ca45260b8a24542d40f7558b5d6bf5018d1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/477975
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index bf026b6..225f3dd 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -1240,6 +1240,42 @@
 	return lockedSnapshot{s}.ReadFile(ctx, uri)
 }
 
+// preloadFiles delegates to the view FileSource to read the requested uris in
+// parallel, without holding the snapshot lock.
+func (s *snapshot) preloadFiles(ctx context.Context, uris []span.URI) {
+	files := make([]source.FileHandle, len(uris))
+	var wg sync.WaitGroup
+	iolimit := make(chan struct{}, 20) // I/O concurrency limiting semaphore
+	for i, uri := range uris {
+		wg.Add(1)
+		iolimit <- struct{}{}
+		go func(i int, uri span.URI) {
+			defer wg.Done()
+			fh, err := s.view.fs.ReadFile(ctx, uri)
+			<-iolimit
+			if err != nil && ctx.Err() == nil {
+				event.Error(ctx, fmt.Sprintf("reading %s", uri), err)
+				return
+			}
+			files[i] = fh
+		}(i, uri)
+	}
+	wg.Wait()
+
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	for i, fh := range files {
+		if fh == nil {
+			continue // error logged above
+		}
+		uri := uris[i]
+		if _, ok := s.files.Get(uri); !ok {
+			s.files.Set(uri, fh)
+		}
+	}
+}
+
 // A lockedSnapshot implements the source.FileSource interface while holding
 // the lock for the wrapped snapshot.
 type lockedSnapshot struct{ wrapped *snapshot }