internal/lsp/cache: consolidate snapshot cloning

Cloning is complicated enough without worrying about concurrency, so
hold the snapshot's lock during the entire process.

Consolidate everything into one function. I don't think that the split
was making it easier to understand, and I was able to see and clean up
some extra complexity once it was all in one place. Let's discuss
options if you think the result is too long.

I don't intend any semantic changes in this CL.

Updates golang/go#35638.

Change-Id: I05c4b28875976293f5fcd56248d9c9e468f85cc6
Reviewed-on: https://go-review.googlesource.com/c/tools/+/211537
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 6fa8030..d6c5e97 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -216,7 +216,10 @@
 func (s *snapshot) getImportedBy(id packageID) []packageID {
 	s.mu.Lock()
 	defer s.mu.Unlock()
+	return s.getImportedByLocked(id)
+}
 
+func (s *snapshot) getImportedByLocked(id packageID) []packageID {
 	// If we haven't rebuilt the import graph since creating the snapshot.
 	if len(s.importedBy) == 0 {
 		s.rebuildImportGraph()
@@ -457,10 +460,66 @@
 	return s.files[f.URI()]
 }
 
-func (s *snapshot) clone(ctx context.Context, withoutURI span.URI, withoutTypes, withoutMetadata map[packageID]struct{}) *snapshot {
+func (s *snapshot) clone(ctx context.Context, withoutFile source.File) *snapshot {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
+	toInvalidate := map[packageID]struct{}{}
+	// Collect all of the package IDs that correspond to the given file.
+	// TODO: if the file has moved into a new package, we should invalidate that too.
+	for _, id := range s.ids[withoutFile.URI()] {
+		toInvalidate[id] = struct{}{}
+	}
+
+	// If we are invalidating a go.mod file then we should invalidate all of the packages in the module
+	if withoutFile.Kind() == source.Mod {
+		for id := range s.workspacePackages {
+			toInvalidate[id] = struct{}{}
+		}
+	}
+
+	// Get the original FileHandle for the URI, if it exists.
+	originalFH := s.files[withoutFile.URI()]
+
+	// If this is a file we don't yet know about,
+	// then we do not yet know what packages it should belong to.
+	// Make a rough estimate of what metadata to invalidate by finding the package IDs
+	// of all of the files in the same directory as this one.
+	// TODO(rstambler): Speed this up by mapping directories to filenames.
+	if originalFH == nil {
+		if dirStat, err := os.Stat(dir(withoutFile.URI().Filename())); err == nil {
+			for uri := range s.files {
+				if fdirStat, err := os.Stat(dir(uri.Filename())); err == nil {
+					if os.SameFile(dirStat, fdirStat) {
+						for _, id := range s.ids[uri] {
+							toInvalidate[id] = struct{}{}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	// If there is no known FileHandle and no known IDs for the given file,
+	// there is nothing to invalidate.
+	if len(toInvalidate) == 0 && originalFH == nil {
+		// TODO(heschi): clone anyway? Seems like this is just setting us up for trouble.
+		return s
+	}
+
+	// Invalidate reverse dependencies too.
+	// TODO(heschi): figure out the locking model and use transitiveReverseDeps?
+	var addRevDeps func(packageID)
+	addRevDeps = func(id packageID) {
+		toInvalidate[id] = struct{}{}
+		for _, rid := range s.getImportedByLocked(id) {
+			addRevDeps(rid)
+		}
+	}
+	for id := range toInvalidate {
+		addRevDeps(id)
+	}
+
 	result := &snapshot{
 		id:                s.id + 1,
 		view:              s.view,
@@ -474,7 +533,7 @@
 	}
 	// Copy all of the FileHandles except for the one that was invalidated.
 	for k, v := range s.files {
-		if k == withoutURI {
+		if k == withoutFile.URI() {
 			continue
 		}
 		result.files[k] = v
@@ -485,35 +544,37 @@
 	}
 	// Copy the package type information.
 	for k, v := range s.packages {
-		if _, ok := withoutTypes[k.id]; ok {
-			continue
-		}
-		if _, ok := withoutMetadata[k.id]; ok {
+		if _, ok := toInvalidate[k.id]; ok {
 			continue
 		}
 		result.packages[k] = v
 	}
 	// Copy the package analysis information.
 	for k, v := range s.actions {
-		if _, ok := withoutTypes[k.pkg.id]; ok {
-			continue
-		}
-		if _, ok := withoutMetadata[k.pkg.id]; ok {
+		if _, ok := toInvalidate[k.pkg.id]; ok {
 			continue
 		}
 		result.actions[k] = v
 	}
-	// Copy the package metadata.
-	for k, v := range s.metadata {
-		if _, ok := withoutMetadata[k]; ok {
-			continue
-		}
-		result.metadata[k] = v
-	}
 	// Copy the set of initally loaded packages.
 	for k, v := range s.workspacePackages {
 		result.workspacePackages[k] = v
 	}
+
+	// Get the current FileHandle for the URI.
+	currentFH := s.view.session.GetFile(withoutFile.URI(), withoutFile.Kind())
+
+	// Check if the file's package name or imports have changed,
+	// and if so, invalidate this file's packages' metadata.
+	invalidateMetadata := s.view.session.cache.shouldLoad(ctx, s, originalFH, currentFH)
+
+	// Copy the package metadata.
+	for k, v := range s.metadata {
+		if _, ok := toInvalidate[k]; invalidateMetadata && ok {
+			continue
+		}
+		result.metadata[k] = v
+	}
 	// Don't bother copying the importedBy graph,
 	// as it changes each time we update metadata.
 	return result
@@ -526,87 +587,17 @@
 // invalidateContent invalidates the content of a Go file,
 // including any position and type information that depends on it.
 // It returns true if we were already tracking the given file, false otherwise.
-//
-// Note: The logic in this function is convoluted. Do not change without significant thought.
-func (v *view) invalidateContent(ctx context.Context, f source.File, action source.FileAction) bool {
-	var (
-		withoutTypes    = make(map[packageID]struct{})
-		withoutMetadata = make(map[packageID]struct{})
-		ids             = make(map[packageID]struct{})
-	)
-
+func (v *view) invalidateContent(ctx context.Context, f source.File, action source.FileAction) {
 	// This should be the only time we hold the view's snapshot lock for any period of time.
 	v.snapshotMu.Lock()
 	defer v.snapshotMu.Unlock()
 
-	// Collect all of the package IDs that correspond to the given file.
-	for _, id := range v.snapshot.getIDs(f.URI()) {
-		ids[id] = struct{}{}
-	}
-
-	// If we are invalidating a go.mod file then we should invalidate all of the packages in the module
-	if f.Kind() == source.Mod {
-		for id := range v.snapshot.workspacePackages {
-			ids[id] = struct{}{}
-		}
-	}
-
-	// Get the original FileHandle for the URI, if it exists.
-	originalFH := v.snapshot.getFile(f.URI())
-
-	// If this is a file we don't yet know about,
-	// then we do not yet know what packages it should belong to.
-	// Make a rough estimate of what metadata to invalidate by finding the package IDs
-	// of all of the files in the same directory as this one.
-	// TODO(rstambler): Speed this up by mapping directories to filenames.
-	if originalFH == nil {
-		if dirStat, err := os.Stat(dir(f.URI().Filename())); err == nil {
-			for _, uri := range v.snapshot.getFileURIs() {
-				if fdirStat, err := os.Stat(dir(uri.Filename())); err == nil {
-					if os.SameFile(dirStat, fdirStat) {
-						for _, id := range v.snapshot.ids[uri] {
-							ids[id] = struct{}{}
-						}
-					}
-				}
-			}
-		}
-		// Make sure that the original FileHandle is nil.
-		originalFH = nil
-	}
-
-	// If there is no known FileHandle and no known IDs for the given file,
-	// there is nothing to invalidate.
-	if len(ids) == 0 && originalFH == nil {
-		return false
-	}
-
-	// Remove the package and all of its reverse dependencies from the cache.
-	for id := range ids {
-		v.snapshot.transitiveReverseDependencies(id, withoutTypes)
-	}
-
 	// If we are deleting a file, make sure to clear out the overlay.
 	if action == source.Delete {
 		v.session.clearOverlay(f.URI())
 	}
 
-	// Get the current FileHandle for the URI.
-	currentFH := v.session.GetFile(f.URI(), f.Kind())
-
-	// Check if the file's package name or imports have changed,
-	// and if so, invalidate this file's packages' metadata.
-	if v.session.cache.shouldLoad(ctx, v.snapshot, originalFH, currentFH) {
-		for id := range ids {
-			withoutMetadata[id] = struct{}{}
-
-			// TODO: If a package's name has changed,
-			// we should invalidate the metadata for the new package name (if it exists).
-		}
-	}
-
-	v.snapshot = v.snapshot.clone(ctx, f.URI(), withoutTypes, withoutMetadata)
-	return true
+	v.snapshot = v.snapshot.clone(ctx, f)
 }
 
 func (s *snapshot) clearAndRebuildImportGraph() {
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 0d87755..613e152 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -377,7 +377,8 @@
 	}
 	v.session.filesWatchMap.Watch(uri, func(action source.FileAction) bool {
 		ctx := xcontext.Detach(ctx)
-		return v.invalidateContent(ctx, f, action)
+		v.invalidateContent(ctx, f, action)
+		return true // we're the only watcher
 	})
 	v.mapFile(uri, f)
 	return f, nil