gopls/internal/regtest: add benchmarks for completions

This change adds benchmarks for completions running against x/tools
codebase. We still maintain support for manually configured completion
benchmarks through the CLI; run instructions are added as comments in
code.

Change-Id: I3bcf7edac183397ad01755b767798925c4bd30ba
Reviewed-on: https://go-review.googlesource.com/c/tools/+/256877
Run-TryBot: Danish Dua <danishdua@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Trust: Danish Dua <danishdua@google.com>
diff --git a/gopls/internal/regtest/bench_test.go b/gopls/internal/regtest/bench_test.go
index 0193968..8a7fbdb 100644
--- a/gopls/internal/regtest/bench_test.go
+++ b/gopls/internal/regtest/bench_test.go
@@ -12,142 +12,95 @@
 	"golang.org/x/tools/internal/lsp/protocol"
 )
 
-var iwlBench = struct {
+func printBenchmarkResults(result testing.BenchmarkResult) {
+	fmt.Println("Benchmark Statistics:")
+	fmt.Println(result.String())
+	fmt.Println(result.MemString())
+}
+
+var iwlOptions struct {
 	workdir string
-}{}
+}
 
 func init() {
-	flag.StringVar(&iwlBench.workdir, "iwl_workdir", "", "if set, run IWL benchmark in this directory")
+	flag.StringVar(&iwlOptions.workdir, "iwl_workdir", "", "if set, run IWL benchmark in this directory")
 }
 
 func TestBenchmarkIWL(t *testing.T) {
-	if iwlBench.workdir == "" {
+	if iwlOptions.workdir == "" {
 		t.Skip("-iwl_workdir not configured")
 	}
-	opts := stressTestOptions(iwlBench.workdir)
+
+	opts := stressTestOptions(iwlOptions.workdir)
 	// Don't skip hooks, so that we can wait for IWL.
 	opts = append(opts, SkipHooks(false))
-	b := testing.Benchmark(func(b *testing.B) {
+
+	results := testing.Benchmark(func(b *testing.B) {
 		for i := 0; i < b.N; i++ {
 			withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {
 				env.Await(InitialWorkspaceLoad)
 			})
 		}
 	})
-	printBench(b)
+
+	printBenchmarkResults(results)
 }
 
-var symbolBench = struct {
+var symbolOptions struct {
 	workdir, query, matcher, style string
 	printResults                   bool
-}{}
+}
 
 func init() {
-	flag.StringVar(&symbolBench.workdir, "symbol_workdir", "", "if set, run symbol benchmark in this directory")
-	flag.StringVar(&symbolBench.query, "symbol_query", "test", "symbol query to use in benchmark")
-	flag.StringVar(&symbolBench.matcher, "symbol_matcher", "", "symbol matcher to use in benchmark")
-	flag.StringVar(&symbolBench.style, "symbol_style", "", "symbol style to use in benchmark")
-	flag.BoolVar(&symbolBench.printResults, "symbol_print_results", false, "whether to print symbol query results")
+	flag.StringVar(&symbolOptions.workdir, "symbol_workdir", "", "if set, run symbol benchmark in this directory")
+	flag.StringVar(&symbolOptions.query, "symbol_query", "test", "symbol query to use in benchmark")
+	flag.StringVar(&symbolOptions.matcher, "symbol_matcher", "", "symbol matcher to use in benchmark")
+	flag.StringVar(&symbolOptions.style, "symbol_style", "", "symbol style to use in benchmark")
+	flag.BoolVar(&symbolOptions.printResults, "symbol_print_results", false, "whether to print symbol query results")
 }
 
 func TestBenchmarkSymbols(t *testing.T) {
-	if symbolBench.workdir == "" {
+	if symbolOptions.workdir == "" {
 		t.Skip("-symbol_workdir not configured")
 	}
-	opts := stressTestOptions(symbolBench.workdir)
+
+	opts := stressTestOptions(symbolOptions.workdir)
 	conf := EditorConfig{}
-	if symbolBench.matcher != "" {
-		conf.SymbolMatcher = &symbolBench.matcher
+	if symbolOptions.matcher != "" {
+		conf.SymbolMatcher = &symbolOptions.matcher
 	}
-	if symbolBench.style != "" {
-		conf.SymbolStyle = &symbolBench.style
+	if symbolOptions.style != "" {
+		conf.SymbolStyle = &symbolOptions.style
 	}
 	opts = append(opts, conf)
+
 	withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {
 		// We can't Await in this test, since we have disabled hooks. Instead, run
 		// one symbol request to completion to ensure all necessary cache entries
 		// are populated.
-		results, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{
-			Query: symbolBench.query,
+		symbols, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{
+			Query: symbolOptions.query,
 		})
 		if err != nil {
 			t.Fatal(err)
 		}
-		if symbolBench.printResults {
+
+		if symbolOptions.printResults {
 			fmt.Println("Results:")
-			for i := 0; i < len(results); i++ {
-				fmt.Printf("\t%d. %s (%s)\n", i, results[i].Name, results[i].ContainerName)
+			for i := 0; i < len(symbols); i++ {
+				fmt.Printf("\t%d. %s (%s)\n", i, symbols[i].Name, symbols[i].ContainerName)
 			}
 		}
-		b := testing.Benchmark(func(b *testing.B) {
+
+		results := testing.Benchmark(func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
 				if _, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{
-					Query: symbolBench.query,
+					Query: symbolOptions.query,
 				}); err != nil {
 					t.Fatal(err)
 				}
 			}
 		})
-		printBench(b)
-	})
-}
-
-func printBench(b testing.BenchmarkResult) {
-	fmt.Println("Benchmark stats:")
-	fmt.Println(b.String())
-	fmt.Println(b.MemString())
-}
-
-func dummyCompletionBenchmarkFunction() { const s = "placeholder"; fmt.Printf("%s", s) }
-
-var completionBench = struct {
-	workdir, fileName, locationRegexp string
-	printResults                      bool
-}{}
-
-func init() {
-	flag.StringVar(&completionBench.workdir, "completion_workdir", "", "if set run completion benchmark in this directory (other benchmark flags expect an x/tools dir)")
-	flag.StringVar(&completionBench.fileName, "completion_file", "internal/lsp/regtest/bench_test.go", "relative path to the file to complete")
-	flag.StringVar(&completionBench.locationRegexp, "completion_regexp", `dummyCompletionBenchmarkFunction.*fmt\.Printf\("%s", s(\))`, "regexp location to complete at")
-	flag.BoolVar(&completionBench.printResults, "completion_print_results", false, "whether to print completion results")
-}
-
-func TestBenchmarkCompletion(t *testing.T) {
-	if completionBench.workdir == "" {
-		t.Skip("-completion_workdir not configured")
-	}
-	opts := stressTestOptions(completionBench.workdir)
-	// Completion gives bad results if IWL is not yet complete, so we must await
-	// it first (and therefore need hooks).
-	opts = append(opts, SkipHooks(false))
-	withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {
-		env.Await(InitialWorkspaceLoad)
-		env.OpenFile(completionBench.fileName)
-		params := &protocol.CompletionParams{}
-		params.Context.TriggerCharacter = "s"
-		params.Context.TriggerKind = protocol.TriggerCharacter
-		params.TextDocument.URI = env.Sandbox.Workdir.URI(completionBench.fileName)
-		params.Position = env.RegexpSearch(completionBench.fileName, completionBench.locationRegexp).ToProtocolPosition()
-
-		// Run one completion to make sure everything is warm.
-		list, err := env.Editor.Server.Completion(env.Ctx, params)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if completionBench.printResults {
-			fmt.Println("Results:")
-			for i := 0; i < len(list.Items); i++ {
-				fmt.Printf("\t%d. %v\n", i, list.Items[i])
-			}
-		}
-		b := testing.Benchmark(func(b *testing.B) {
-			for i := 0; i < b.N; i++ {
-				_, err := env.Editor.Server.Completion(env.Ctx, params)
-				if err != nil {
-					t.Fatal(err)
-				}
-			}
-		})
-		printBench(b)
+		printBenchmarkResults(results)
 	})
 }
diff --git a/gopls/internal/regtest/completion_bench_test.go b/gopls/internal/regtest/completion_bench_test.go
new file mode 100644
index 0000000..1b6fe46
--- /dev/null
+++ b/gopls/internal/regtest/completion_bench_test.go
@@ -0,0 +1,201 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package regtest
+
+import (
+	"flag"
+	"fmt"
+	"runtime"
+	"strings"
+	"testing"
+
+	"golang.org/x/tools/internal/lsp/fake"
+)
+
+// dummyCompletionFunction to test manually configured completion using CLI.
+func dummyCompletionFunction() { const s = "placeholder"; fmt.Printf("%s", s) }
+
+type completionBenchOptions struct {
+	workdir, file, locationRegexp string
+	printResults                  bool
+	// hook to run edits before initial completion, not supported for manually
+	// configured completions.
+	preCompletionEdits func(*Env)
+}
+
+var completionOptions = completionBenchOptions{}
+
+func init() {
+	flag.StringVar(&completionOptions.workdir, "completion_workdir", "", "directory to run completion benchmarks in")
+	flag.StringVar(&completionOptions.file, "completion_file", "", "relative path to the file to complete in")
+	flag.StringVar(&completionOptions.locationRegexp, "completion_regexp", "", "regexp location to complete at")
+	flag.BoolVar(&completionOptions.printResults, "completion_print_results", false, "whether to print completion results")
+}
+
+func benchmarkCompletion(options completionBenchOptions, t *testing.T) {
+	if completionOptions.workdir == "" {
+		t.Skip("-completion_workdir not configured, skipping benchmark")
+	}
+
+	opts := stressTestOptions(options.workdir)
+
+	// Completion gives bad results if IWL is not yet complete, so we must await
+	// it first (and therefore need hooks).
+	opts = append(opts, SkipHooks(false))
+
+	withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {
+		env.Await(InitialWorkspaceLoad)
+		env.OpenFile(options.file)
+
+		// Run edits required for this completion.
+		if options.preCompletionEdits != nil {
+			options.preCompletionEdits(env)
+		}
+
+		// Add a comment as a marker at the start of the file, we'll replace
+		// this in every iteration to trigger type checking and hence emulate
+		// a more real world scenario.
+		env.EditBuffer(options.file, fake.Edit{Text: "// 0\n"})
+
+		// Run a completion to make sure the system is warm.
+		pos := env.RegexpSearch(options.file, options.locationRegexp)
+		completions := env.Completion(options.file, pos)
+
+		if options.printResults {
+			fmt.Println("Results:")
+			for i := 0; i < len(completions.Items); i++ {
+				fmt.Printf("\t%d. %v\n", i, completions.Items[i])
+			}
+		}
+
+		results := testing.Benchmark(func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				b.StopTimer()
+				env.RegexpReplace(options.file, `\/\/ \d*`, fmt.Sprintf("// %d", i))
+
+				// explicitly garbage collect since we don't want to count this
+				// time in completion benchmarks.
+				if i%10 == 0 {
+					runtime.GC()
+				}
+				b.StartTimer()
+
+				env.Completion(options.file, pos)
+			}
+		})
+
+		printBenchmarkResults(results)
+	})
+}
+
+// endPosInBuffer returns the position for last character in the buffer for
+// the given file.
+func endPosInBuffer(env *Env, name string) fake.Pos {
+	buffer := env.Editor.BufferText(name)
+	lines := strings.Split(buffer, "\n")
+	numLines := len(lines)
+
+	return fake.Pos{
+		Line:   numLines - 1,
+		Column: len([]rune(lines[numLines-1])),
+	}
+}
+
+// Benchmark completion at a specified file and location. When no CLI options
+// are specified, this test is skipped.
+// To Run (from x/tools/gopls) against the dummy function above:
+// 	go test -v ./internal/regtest -run=TestBenchmarkConfiguredCompletion
+// 	-completion_workdir="$HOME/Developer/tools"
+// 	-completion_file="gopls/internal/regtest/completion_bench_test.go"
+// 	-completion_regexp="dummyCompletionFunction.*fmt\.Printf\(\"%s\", s(\))"
+func TestBenchmarkConfiguredCompletion(t *testing.T) {
+	benchmarkCompletion(completionOptions, t)
+}
+
+// To run (from x/tools/gopls):
+// 	go test -v ./internal/regtest -run TestBenchmark<>Completion
+//	-completion_workdir="$HOME/Developer/tools"
+// where <> is one of the tests below. completion_workdir should be path to
+// x/tools on your system.
+
+// Benchmark struct completion in tools codebase.
+func TestBenchmarkStructCompletion(t *testing.T) {
+	file := "internal/lsp/cache/session.go"
+
+	preCompletionEdits := func(env *Env) {
+		env.OpenFile(file)
+		originalBuffer := env.Editor.BufferText(file)
+		env.EditBuffer(file, fake.Edit{
+			End:  endPosInBuffer(env, file),
+			Text: originalBuffer + "\nvar testVariable map[string]bool = Session{}.\n",
+		})
+	}
+
+	benchmarkCompletion(completionBenchOptions{
+		workdir:            completionOptions.workdir,
+		file:               file,
+		locationRegexp:     `var testVariable map\[string\]bool = Session{}(\.)`,
+		preCompletionEdits: preCompletionEdits,
+		printResults:       completionOptions.printResults,
+	}, t)
+}
+
+// Benchmark import completion in tools codebase.
+func TestBenchmarkImportCompletion(t *testing.T) {
+	benchmarkCompletion(completionBenchOptions{
+		workdir:        completionOptions.workdir,
+		file:           "internal/lsp/source/completion/completion.go",
+		locationRegexp: `go\/()`,
+		printResults:   completionOptions.printResults,
+	}, t)
+}
+
+// Benchmark slice completion in tools codebase.
+func TestBenchmarkSliceCompletion(t *testing.T) {
+	file := "internal/lsp/cache/session.go"
+
+	preCompletionEdits := func(env *Env) {
+		env.OpenFile(file)
+		originalBuffer := env.Editor.BufferText(file)
+		env.EditBuffer(file, fake.Edit{
+			End:  endPosInBuffer(env, file),
+			Text: originalBuffer + "\nvar testVariable []byte = \n",
+		})
+	}
+
+	benchmarkCompletion(completionBenchOptions{
+		workdir:            completionOptions.workdir,
+		file:               file,
+		locationRegexp:     `var testVariable \[\]byte (=)`,
+		preCompletionEdits: preCompletionEdits,
+		printResults:       completionOptions.printResults,
+	}, t)
+}
+
+// Benchmark deep completion in function call in tools codebase.
+func TestBenchmarkFuncDeepCompletion(t *testing.T) {
+	file := "internal/lsp/source/completion/completion.go"
+	fileContent := `
+func (c *completer) _() {
+	c.inference.kindMatches(c.)
+}
+`
+	preCompletionEdits := func(env *Env) {
+		env.OpenFile(file)
+		originalBuffer := env.Editor.BufferText(file)
+		env.EditBuffer(file, fake.Edit{
+			End:  endPosInBuffer(env, file),
+			Text: originalBuffer + fileContent,
+		})
+	}
+
+	benchmarkCompletion(completionBenchOptions{
+		workdir:            completionOptions.workdir,
+		file:               file,
+		locationRegexp:     `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
+		preCompletionEdits: preCompletionEdits,
+		printResults:       completionOptions.printResults,
+	}, t)
+}
diff --git a/gopls/internal/regtest/completion_test.go b/gopls/internal/regtest/completion_test.go
index 7cc0b6a..2651f59 100644
--- a/gopls/internal/regtest/completion_test.go
+++ b/gopls/internal/regtest/completion_test.go
@@ -131,10 +131,8 @@
 					)
 				}
 				env.OpenFile(tc.filename)
-				completions, err := env.Editor.Completion(env.Ctx, tc.filename, env.RegexpSearch(tc.filename, tc.triggerRegexp))
-				if err != nil {
-					t.Fatal(err)
-				}
+				completions := env.Completion(tc.filename, env.RegexpSearch(tc.filename, tc.triggerRegexp))
+
 				// Check that the completion item suggestions are in the range
 				// of the file.
 				lineCount := len(strings.Split(env.Editor.BufferText(tc.filename), "\n"))
@@ -183,13 +181,10 @@
 	want := []string{"ma", "ma_test", "main", "math", "math_test"}
 	run(t, files, func(t *testing.T, env *Env) {
 		env.OpenFile("math/add.go")
-		completions, err := env.Editor.Completion(env.Ctx, "math/add.go", fake.Pos{
+		completions := env.Completion("math/add.go", fake.Pos{
 			Line:   0,
 			Column: 10,
 		})
-		if err != nil {
-			t.Fatal(err)
-		}
 
 		diff := compareCompletionResults(want, completions.Items)
 		if diff != "" {
diff --git a/gopls/internal/regtest/wrappers.go b/gopls/internal/regtest/wrappers.go
index d00712a..8d8136f 100644
--- a/gopls/internal/regtest/wrappers.go
+++ b/gopls/internal/regtest/wrappers.go
@@ -270,6 +270,16 @@
 	return locations
 }
 
+// Completion executes a completion request on the server.
+func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList {
+	e.T.Helper()
+	completions, err := e.Editor.Completion(e.Ctx, path, pos)
+	if err != nil {
+		e.T.Fatal(err)
+	}
+	return completions
+}
+
 // CodeAction calls testDocument/codeAction for the given path, and calls
 // t.Fatal if there are errors.
 func (e *Env) CodeAction(path string) []protocol.CodeAction {
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index f9a5743..38546af 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -178,6 +178,7 @@
 		"verboseWorkDoneProgress": true,
 		"env":                     e.overlayEnv(),
 		"expandWorkspaceToModule": !e.Config.LimitWorkspaceScope,
+		"completionBudget":        "10s",
 	}
 
 	if e.Config.CodeLens != nil {