internal/lsp/cache: cache known subdirs pattern

Known subdirs change rarely and it's quite expensive to compute
a glob pattern derived from them, so cache computation result
and inherit it across generations.

Computation cost is divided ≈evenly between `sort.Sort` and
`span.URI.Filename` calls, and there is no trivial way to optimize
them away besides caching.

Benchmark (didChange in kubernetes): ~37ms->30ms

Change-Id: Idb1691c76b8ff163dc61f637f07229498888606c
GitHub-Last-Rev: cd99a9ce5c797afb5aaa9b478fcf433edd0dc03c
GitHub-Pull-Request: golang/tools#383
Reviewed-on: https://go-review.googlesource.com/c/tools/+/411636
Reviewed-by: Alan Donovan <adonovan@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 6edd1db..7b73f4b 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -114,7 +114,8 @@
 
 	// knownSubdirs is the set of subdirectories in the workspace, used to
 	// create glob patterns for file watching.
-	knownSubdirs map[span.URI]struct{}
+	knownSubdirs             map[span.URI]struct{}
+	knownSubdirsPatternCache string
 	// unprocessedSubdirChanges are any changes that might affect the set of
 	// subdirectories in the workspace. They are not reflected to knownSubdirs
 	// during the snapshot cloning step as it can slow down cloning.
@@ -834,21 +835,38 @@
 	// of the directories in the workspace. We find them by adding the
 	// directories of every file in the snapshot's workspace directories.
 	// There may be thousands.
-	knownSubdirs := s.getKnownSubdirs(dirs)
-	if n := len(knownSubdirs); n > 0 {
-		dirNames := make([]string, 0, n)
-		for _, uri := range knownSubdirs {
-			dirNames = append(dirNames, uri.Filename())
-		}
-		sort.Strings(dirNames)
-		// The double allocation of Sprintf(Join()) accounts for 8%
-		// of DidChange, but specializing doesn't appear to help. :(
-		patterns[fmt.Sprintf("{%s}", strings.Join(dirNames, ","))] = struct{}{}
+	if pattern := s.getKnownSubdirsPattern(dirs); pattern != "" {
+		patterns[pattern] = struct{}{}
 	}
 
 	return patterns
 }
 
+func (s *snapshot) getKnownSubdirsPattern(wsDirs []span.URI) string {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	// First, process any pending changes and update the set of known
+	// subdirectories.
+	// It may change list of known subdirs and therefore invalidate the cache.
+	s.applyKnownSubdirsChangesLocked(wsDirs)
+
+	if len(s.knownSubdirs) == 0 {
+		return ""
+	}
+
+	if s.knownSubdirsPatternCache == "" {
+		dirNames := make([]string, 0, len(s.knownSubdirs))
+		for uri := range s.knownSubdirs {
+			dirNames = append(dirNames, uri.Filename())
+		}
+		sort.Strings(dirNames)
+		s.knownSubdirsPatternCache = fmt.Sprintf("{%s}", strings.Join(dirNames, ","))
+	}
+
+	return s.knownSubdirsPatternCache
+}
+
 // collectAllKnownSubdirs collects all of the subdirectories within the
 // snapshot's workspace directories. None of the workspace directories are
 // included.
@@ -859,6 +877,7 @@
 	defer s.mu.Unlock()
 
 	s.knownSubdirs = map[span.URI]struct{}{}
+	s.knownSubdirsPatternCache = ""
 	for uri := range s.files {
 		s.addKnownSubdirLocked(uri, dirs)
 	}
@@ -870,6 +889,16 @@
 
 	// First, process any pending changes and update the set of known
 	// subdirectories.
+	s.applyKnownSubdirsChangesLocked(wsDirs)
+
+	result := make([]span.URI, 0, len(s.knownSubdirs))
+	for uri := range s.knownSubdirs {
+		result = append(result, uri)
+	}
+	return result
+}
+
+func (s *snapshot) applyKnownSubdirsChangesLocked(wsDirs []span.URI) {
 	for _, c := range s.unprocessedSubdirChanges {
 		if c.isUnchanged {
 			continue
@@ -881,12 +910,6 @@
 		}
 	}
 	s.unprocessedSubdirChanges = nil
-
-	result := make([]span.URI, 0, len(s.knownSubdirs))
-	for uri := range s.knownSubdirs {
-		result = append(result, uri)
-	}
-	return result
 }
 
 func (s *snapshot) addKnownSubdirLocked(uri span.URI, dirs []span.URI) {
@@ -917,6 +940,7 @@
 		}
 		s.knownSubdirs[uri] = struct{}{}
 		dir = filepath.Dir(dir)
+		s.knownSubdirsPatternCache = ""
 	}
 }
 
@@ -929,6 +953,7 @@
 		}
 		if info, _ := os.Stat(dir); info == nil {
 			delete(s.knownSubdirs, uri)
+			s.knownSubdirsPatternCache = ""
 		}
 		dir = filepath.Dir(dir)
 	}
@@ -1816,6 +1841,7 @@
 	for k, v := range s.knownSubdirs {
 		result.knownSubdirs[k] = v
 	}
+	result.knownSubdirsPatternCache = s.knownSubdirsPatternCache
 	for _, c := range changes {
 		result.unprocessedSubdirChanges = append(result.unprocessedSubdirChanges, c)
 	}