internal/lsp/regtest: add benchmarks for IWL and completion

Add additional benchmarks following the pattern of symbol benchmarks.
One for initial workspace load, and another for completion.

Change-Id: Iba826b188cb81dffabb1b08287dc7b76250dc54c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/250802
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/fake/edit.go b/internal/lsp/fake/edit.go
index fb28841..e5be4f6 100644
--- a/internal/lsp/fake/edit.go
+++ b/internal/lsp/fake/edit.go
@@ -25,7 +25,7 @@
 	End   Pos
 }
 
-func (p Pos) toProtocolPosition() protocol.Position {
+func (p Pos) ToProtocolPosition() protocol.Position {
 	return protocol.Position{
 		Line:      float64(p.Line),
 		Character: float64(p.Column),
@@ -73,8 +73,8 @@
 func (e Edit) toProtocolChangeEvent() protocol.TextDocumentContentChangeEvent {
 	return protocol.TextDocumentContentChangeEvent{
 		Range: &protocol.Range{
-			Start: e.Start.toProtocolPosition(),
-			End:   e.End.toProtocolPosition(),
+			Start: e.Start.ToProtocolPosition(),
+			End:   e.End.ToProtocolPosition(),
 		},
 		Text: e.Text,
 	}
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index 1a89f9a..0f104e4 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -579,7 +579,7 @@
 	}
 	params := &protocol.DefinitionParams{}
 	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
-	params.Position = pos.toProtocolPosition()
+	params.Position = pos.ToProtocolPosition()
 
 	resp, err := e.Server.Definition(ctx, params)
 	if err != nil {
@@ -802,7 +802,7 @@
 	params := &protocol.ReferenceParams{
 		TextDocumentPositionParams: protocol.TextDocumentPositionParams{
 			TextDocument: e.textDocumentIdentifier(path),
-			Position:     pos.toProtocolPosition(),
+			Position:     pos.ToProtocolPosition(),
 		},
 		Context: protocol.ReferenceContext{
 			IncludeDeclaration: true,
@@ -846,7 +846,7 @@
 	}
 	params := &protocol.HoverParams{}
 	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
-	params.Position = pos.toProtocolPosition()
+	params.Position = pos.ToProtocolPosition()
 
 	resp, err := e.Server.Hover(ctx, params)
 	if err != nil {
diff --git a/internal/lsp/regtest/bench_test.go b/internal/lsp/regtest/bench_test.go
index d91c424..763e547 100644
--- a/internal/lsp/regtest/bench_test.go
+++ b/internal/lsp/regtest/bench_test.go
@@ -9,10 +9,38 @@
 	"fmt"
 	"testing"
 
+	"golang.org/x/tools/internal/lsp"
 	"golang.org/x/tools/internal/lsp/fake"
 	"golang.org/x/tools/internal/lsp/protocol"
 )
 
+var iwlBench = struct {
+	workdir string
+}{}
+
+func init() {
+	flag.StringVar(&iwlBench.workdir, "iwl_workdir", "", "if set, run IWL benchmark in this directory")
+}
+
+func TestBenchmarkIWL(t *testing.T) {
+	if iwlBench.workdir == "" {
+		t.Skip("-iwl_workdir not configured")
+	}
+	opts := stressTestOptions(iwlBench.workdir)
+	// Don't skip hooks, so that we can wait for IWL.
+	opts = append(opts, SkipHooks(false))
+	b := 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(
+					CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1),
+				)
+			})
+		}
+	})
+	printBench(b)
+}
+
 var symbolBench = struct {
 	workdir, query, matcher, style string
 	printResults                   bool
@@ -23,7 +51,7 @@
 	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, "symbol style to use in benchmark")
+	flag.BoolVar(&symbolBench.printResults, "symbol_print_results", false, "whether to print symbol query results")
 }
 
 func TestBenchmarkSymbols(t *testing.T) {
@@ -64,8 +92,68 @@
 				}
 			}
 		})
-		fmt.Println("Benchmark stats:")
-		fmt.Println(b.String())
-		fmt.Println(b.MemString())
+		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(
+			CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1),
+		)
+		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)
 	})
 }
diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go
index 1b042f5..15bac64 100644
--- a/internal/lsp/regtest/runner.go
+++ b/internal/lsp/regtest/runner.go
@@ -176,12 +176,12 @@
 	})
 }
 
-// NoHooks disables the test runner's client hooks that are used for
-// instrumenting expectations (tracking diagnostics, logs, work done, etc.). It
-// is intended for performance-sensitive stress tests.
-func NoHooks() RunOption {
+// SkipHooks allows for disabling the test runner's client hooks that are used
+// for instrumenting expectations (tracking diagnostics, logs, work done,
+// etc.). It is intended for performance-sensitive stress tests or benchmarks.
+func SkipHooks(skip bool) RunOption {
 	return optionSetter(func(opts *runConfig) {
-		opts.skipHooks = true
+		opts.skipHooks = skip
 	})
 }
 
diff --git a/internal/lsp/regtest/stress_test.go b/internal/lsp/regtest/stress_test.go
index 55e81ae..b3f3a3b 100644
--- a/internal/lsp/regtest/stress_test.go
+++ b/internal/lsp/regtest/stress_test.go
@@ -29,9 +29,10 @@
 		// Enable live debugging.
 		WithDebugAddress(":8087"),
 
-		// Skip logs and hooks, as they buffer up memory unnaturally.
+		// Skip logs as they buffer up memory unnaturally.
 		SkipLogs(),
-		NoHooks(),
+		// Similarly to logs: disable hooks so that they don't affect performance.
+		SkipHooks(true),
 		// The Debug server only makes sense if running in singleton mode.
 		WithModes(Singleton),
 		// Set a generous timeout. Individual tests should control their own