gopls/internal/regtest/bench: add a benchmark for diagnosing a change

Add a benchmark that measures how long it takes gopls to fully complete
diagnostics from a change.

Change-Id: Icb19ccf658ebe6604337283cb6e13773c3b95f18
Reviewed-on: https://go-review.googlesource.com/c/tools/+/472875
Reviewed-by: Alan Donovan <adonovan@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/regtest/bench/didchange_test.go b/gopls/internal/regtest/bench/didchange_test.go
index bb639ee..3a7cb10 100644
--- a/gopls/internal/regtest/bench/didchange_test.go
+++ b/gopls/internal/regtest/bench/didchange_test.go
@@ -6,41 +6,47 @@
 
 import (
 	"fmt"
+	"sync/atomic"
 	"testing"
+	"time"
 
+	"golang.org/x/tools/gopls/internal/lsp/fake"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 )
 
+// Use a global edit counter as bench function may execute multiple times, and
+// we want to avoid cache hits. Use time.Now to also avoid cache hits from the
+// shared file cache.
+var editID int64 = time.Now().UnixNano()
+
+var didChangeTests = []struct {
+	repo string
+	file string
+}{
+	{"istio", "pkg/fuzz/util.go"},
+	{"kubernetes", "pkg/controller/lookup_cache.go"},
+	{"kuma", "api/generic/insights.go"},
+	{"pkgsite", "internal/frontend/server.go"},
+	{"starlark", "starlark/eval.go"},
+	{"tools", "internal/lsp/cache/snapshot.go"},
+}
+
 // BenchmarkDidChange benchmarks modifications of a single file by making
 // synthetic modifications in a comment. It controls pacing by waiting for the
 // server to actually start processing the didChange notification before
 // proceeding. Notably it does not wait for diagnostics to complete.
 func BenchmarkDidChange(b *testing.B) {
-	tests := []struct {
-		repo string
-		file string
-	}{
-		{"istio", "pkg/fuzz/util.go"},
-		{"kubernetes", "pkg/controller/lookup_cache.go"},
-		{"kuma", "api/generic/insights.go"},
-		{"pkgsite", "internal/frontend/server.go"},
-		{"starlark", "starlark/eval.go"},
-		{"tools", "internal/lsp/cache/snapshot.go"},
-	}
-
-	for _, test := range tests {
-		edits := 0 // bench function may execute multiple times
+	for _, test := range didChangeTests {
 		b.Run(test.repo, func(b *testing.B) {
 			env := getRepo(b, test.repo).sharedEnv(b)
 			env.OpenFile(test.file)
-			env.AfterChange()
 			// Insert the text we'll be modifying at the top of the file.
 			env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"})
 			env.AfterChange()
 			b.ResetTimer()
 
 			for i := 0; i < b.N; i++ {
-				edits++
+				edits := atomic.AddInt64(&editID, 1)
 				env.EditBuffer(test.file, protocol.TextEdit{
 					Range: protocol.Range{
 						Start: protocol.Position{Line: 0, Character: 0},
@@ -54,3 +60,40 @@
 		})
 	}
 }
+
+func BenchmarkDiagnoseChange(b *testing.B) {
+	for _, test := range didChangeTests {
+		b.Run(test.repo, func(b *testing.B) {
+			// Use a new env to avoid the diagnostic delay: we want to measure how
+			// long it takes to produce the diagnostics.
+			env := repos[test.repo].newEnv(b, "diagnoseChange", fake.EditorConfig{
+				Settings: map[string]interface{}{
+					"diagnosticsDelay": "0s",
+				},
+			})
+			env.OpenFile(test.file)
+			// Insert the text we'll be modifying at the top of the file.
+			env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"})
+			env.AfterChange()
+			b.ResetTimer()
+
+			// We must use an extra subtest layer here, so that we only set up the
+			// shared env once (otherwise we pay additional overhead and the profiling
+			// flags don't work).
+			b.Run("diagnose", func(b *testing.B) {
+				for i := 0; i < b.N; i++ {
+					edits := atomic.AddInt64(&editID, 1)
+					env.EditBuffer(test.file, protocol.TextEdit{
+						Range: protocol.Range{
+							Start: protocol.Position{Line: 0, Character: 0},
+							End:   protocol.Position{Line: 1, Character: 0},
+						},
+						// Increment the placeholder text, to ensure cache misses.
+						NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits),
+					})
+					env.AfterChange()
+				}
+			})
+		})
+	}
+}