gopls/internal/cache: memoize dependent hash on analysisNode

Benchmarking demonstrated a nontrivial amount of time spent hashing
dependency information in the computation of analysisNode.cacheKey.
Memoize this hash to reduce cost.

Change-Id: Ic123202fbdf00c9de7b3f697c40f593ef798cd42
Reviewed-on: https://go-review.googlesource.com/c/tools/+/617395
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go
index 0d7fc46..9debc60 100644
--- a/gopls/internal/cache/analysis.go
+++ b/gopls/internal/cache/analysis.go
@@ -539,6 +539,9 @@
 	typesOnce sync.Once      // guards lazy population of types and typesErr fields
 	types     *types.Package // type information lazily imported from summary
 	typesErr  error          // an error producing type information
+
+	depHashOnce sync.Once
+	_depHash    file.Hash // memoized hash of data affecting dependents
 }
 
 func (an *analysisNode) String() string { return string(an.mp.ID) }
@@ -597,6 +600,40 @@
 	return an.types, an.typesErr
 }
 
+// depHash computes the hash of node information that may affect other nodes
+// depending on this node: the package path, export hash, and action results.
+//
+// The result is memoized to avoid redundant work when analysing multiple
+// dependents.
+func (an *analysisNode) depHash() file.Hash {
+	an.depHashOnce.Do(func() {
+		hasher := sha256.New()
+		fmt.Fprintf(hasher, "dep: %s\n", an.mp.PkgPath)
+		fmt.Fprintf(hasher, "export: %s\n", an.summary.DeepExportHash)
+
+		// action results: errors and facts
+		actions := an.summary.Actions
+		names := make([]string, 0, len(actions))
+		for name := range actions {
+			names = append(names, name)
+		}
+		sort.Strings(names)
+		for _, name := range names {
+			summary := actions[name]
+			fmt.Fprintf(hasher, "action %s\n", name)
+			if summary.Err != "" {
+				fmt.Fprintf(hasher, "error %s\n", summary.Err)
+			} else {
+				fmt.Fprintf(hasher, "facts %s\n", summary.FactsHash)
+				// We can safely omit summary.diagnostics
+				// from the key since they have no downstream effect.
+			}
+		}
+		hasher.Sum(an._depHash[:0])
+	})
+	return an._depHash
+}
+
 // analyzeSummary is a gob-serializable summary of successfully
 // applying a list of analyzers to a package.
 type analyzeSummary struct {
@@ -770,27 +807,8 @@
 
 	// vdeps, in PackageID order
 	for _, vdep := range moremaps.Sorted(an.succs) {
-		fmt.Fprintf(hasher, "dep: %s\n", vdep.mp.PkgPath)
-		fmt.Fprintf(hasher, "export: %s\n", vdep.summary.DeepExportHash)
-
-		// action results: errors and facts
-		actions := vdep.summary.Actions
-		names := make([]string, 0, len(actions))
-		for name := range actions {
-			names = append(names, name)
-		}
-		sort.Strings(names)
-		for _, name := range names {
-			summary := actions[name]
-			fmt.Fprintf(hasher, "action %s\n", name)
-			if summary.Err != "" {
-				fmt.Fprintf(hasher, "error %s\n", summary.Err)
-			} else {
-				fmt.Fprintf(hasher, "facts %s\n", summary.FactsHash)
-				// We can safely omit summary.diagnostics
-				// from the key since they have no downstream effect.
-			}
-		}
+		hash := vdep.depHash()
+		hasher.Write(hash[:])
 	}
 
 	var hash [sha256.Size]byte